diff --git a/cmd/cephctl/main.go b/cmd/cephctl/main.go index 88fac4b..1952cd3 100644 --- a/cmd/cephctl/main.go +++ b/cmd/cephctl/main.go @@ -2,143 +2,16 @@ package main import ( "context" - "fmt" "os" - - kingpin "github.com/alecthomas/kingpin/v2" - log "github.com/sirupsen/logrus" - - "github.com/runityru/cephctl/ceph" - applyCmd "github.com/runityru/cephctl/commands/apply" - diffCmd "github.com/runityru/cephctl/commands/diff" - dumpCephConfigCmd "github.com/runityru/cephctl/commands/dump/cephconfig" - dumpCephOSDConfigCmd "github.com/runityru/cephctl/commands/dump/cephosdconfig" - healthcheckCmd "github.com/runityru/cephctl/commands/healthcheck" - "github.com/runityru/cephctl/differ" - "github.com/runityru/cephctl/printer" - "github.com/runityru/cephctl/service" -) - -var ( - appVersion = "n/a (dev build)" - buildTimestamp = "undefined" - - app = kingpin.New("cephctl", "Small utility to control Ceph cluster configuration just like any other declarative configuration") - - cephBinary = app. - Flag("ceph-binary", "Specify path to ceph binary"). - Short('b'). - Envar("CEPHCTL_CEPH_BINARY"). - Default("/usr/bin/ceph"). - String() - - debug = app. - Flag("debug", "Enable debug mode"). - Short('d'). - Envar("CEPHCTL_DEBUG"). - Bool() - - trace = app. - Flag("trace", "Enable trace mode (debug mode on steroids)"). - Short('t'). - Envar("CEPHCTL_TRACE"). - Bool() - - colorize = app. - Flag("color", "Colorize diff output"). - Short('c'). - Envar("CEPHCTL_COLOR"). - Default("true"). - Bool() - - apply = app.Command("apply", "Apply ceph configuration") - applySpecFile = apply.Arg("filename", "Filename with configuration specification").Required().String() - - diff = app.Command("diff", "Show difference between running and desired configurations") - - diffSpecFile = diff.Arg("filename", "Filename with configuration specification").Required().String() - - dump = app.Command("dump", "Dump runtime configuration") - dumpCephConfig = dump.Command("cephconfig", "dump Ceph runtime configuration") - dumpCephOSDConfig = dump.Command("cephosdconfig", "dump Ceph OSD configuration") - - healthcheck = app.Command("healthcheck", "Perform a cluster healthcheck and print report") - - version = app.Command("version", "Print version and exit") ) func main() { - ctx := context.Background() - appCmd := kingpin.MustParse(app.Parse(os.Args[1:])) - - if *trace { - log.SetLevel(log.TraceLevel) - log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - }) - log.Trace("Trace mode is enabled. Beware of verbosity!") - } else if *debug { - log.SetLevel(log.DebugLevel) - log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - }) - log.Debug("Debug mode is enabled.") - } - - svc := service.New(ceph.New(*cephBinary), differ.New()) - prntr := printer.New(*colorize) - - switch appCmd { - case apply.FullCommand(): - log.Debug("running apply command") - if err := applyCmd.Apply(ctx, applyCmd.ApplyConfig{ - Service: svc, - SpecFile: *applySpecFile, - }); err != nil { - panic(err) - } - - case diff.FullCommand(): - log.Debug("running diff command") - if err := diffCmd.Diff(ctx, diffCmd.DiffConfig{ - Printer: prntr, - Service: svc, - SpecFile: *diffSpecFile, - }); err != nil { - panic(err) - } - - case dumpCephConfig.FullCommand(): - log.Debug("running dump cephconfig command") - if err := dumpCephConfigCmd.DumpCephConfig(ctx, dumpCephConfigCmd.DumpCephConfigConfig{ - Printer: prntr, - Service: svc, - }); err != nil { - panic(err) - } - - case dumpCephOSDConfig.FullCommand(): - log.Debug("running dump cephosdconfig command") - if err := dumpCephOSDConfigCmd.DumpCephOSDConfig(ctx, dumpCephOSDConfigCmd.DumpCephOSDConfigConfig{ - Printer: prntr, - Service: svc, - }); err != nil { - panic(err) - } - - case healthcheck.FullCommand(): - if err := healthcheckCmd.Healthcheck(ctx, healthcheckCmd.HealthcheckConfig{ - Printer: prntr, - Service: svc, - }); err != nil { - panic(err) - } - - case version.FullCommand(): - fmt.Printf( - "%s v%s / built at %s\n", - os.Args[0], appVersion, buildTimestamp, - ) + // ExecuteContext runs the cobra command tree. + // + // SilenceUsage=true: usage is not printed when a RunE handler returns an + // error. SilenceErrors=false (default): cobra prints the error text to + // stderr (ErrOrStderr), after which we exit with code 1. + if err := rootCmd.ExecuteContext(context.Background()); err != nil { os.Exit(1) } } diff --git a/cmd/cephctl/root.go b/cmd/cephctl/root.go new file mode 100644 index 0000000..1f3f213 --- /dev/null +++ b/cmd/cephctl/root.go @@ -0,0 +1,196 @@ +package main + +import ( + "os" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/runityru/cephctl/ceph" + applyCmd "github.com/runityru/cephctl/commands/apply" + diffCmd "github.com/runityru/cephctl/commands/diff" + dumpCephConfigCmd "github.com/runityru/cephctl/commands/dump/cephconfig" + dumpCephOSDConfigCmd "github.com/runityru/cephctl/commands/dump/cephosdconfig" + healthcheckCmd "github.com/runityru/cephctl/commands/healthcheck" + "github.com/runityru/cephctl/differ" + "github.com/runityru/cephctl/printer" + "github.com/runityru/cephctl/service" +) + +// Variables are bound to the root command's persistent flags via BoolVar/ +// StringVarP. Their values are updated when cmd.Flags().Set(...) is called +// (including from env). +var ( + cephBinary string + debug bool + trace bool + colorize bool +) + +var rootCmd = &cobra.Command{ + Use: "cephctl", + Short: "Small utility to control Ceph cluster configuration just like any other declarative configuration", + // SilenceUsage: do not print usage when a RunE handler returns an error. + SilenceUsage: true, + // SilenceErrors is left false (default): cobra prints the error text to + // stderr, and main() terminates the process with code 1. + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Env support: an environment variable value is applied as the default. + // If the flag was set explicitly by the user (f.Changed), env is ignored. + // This mirrors kingpin's Envar semantics (env = default, flag overrides). + applyStringEnv(cmd, "ceph-binary", "CEPHCTL_CEPH_BINARY") + applyBoolEnv(cmd, "debug", "CEPHCTL_DEBUG") + applyBoolEnv(cmd, "trace", "CEPHCTL_TRACE") + applyBoolEnv(cmd, "color", "CEPHCTL_COLOR") + + if trace { + log.SetLevel(log.TraceLevel) + log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) + log.Trace("Trace mode is enabled. Beware of verbosity!") + } else if debug { + log.SetLevel(log.DebugLevel) + log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) + log.Debug("Debug mode is enabled.") + } + }, + // Without a subcommand, print the root command's help. + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + // Global (persistent) flags — semantics preserved from the kingpin version. + rootCmd.PersistentFlags().StringVarP(&cephBinary, "ceph-binary", "b", "/usr/bin/ceph", "Specify path to ceph binary") + rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Enable debug mode") + rootCmd.PersistentFlags().BoolVarP(&trace, "trace", "t", false, "Enable trace mode (debug mode on steroids)") + rootCmd.PersistentFlags().BoolVarP(&colorize, "color", "c", true, "Colorize diff output") + + rootCmd.AddCommand(applyCommand) + rootCmd.AddCommand(diffCommand) + rootCmd.AddCommand(dumpCommand) + rootCmd.AddCommand(healthcheckCommand) + rootCmd.AddCommand(versionCommand) + + dumpCommand.AddCommand(dumpCephConfigCommand) + dumpCommand.AddCommand(dumpCephOSDConfigCommand) +} + +var applyCommand = &cobra.Command{ + Use: "apply ", + Short: "Apply ceph configuration", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + log.Debug("running apply command") + svc := service.New(ceph.New(cephBinary), differ.New()) + return applyCmd.Apply(cmd.Context(), applyCmd.ApplyConfig{ + Service: svc, + SpecFile: args[0], + }) + }, +} + +var diffCommand = &cobra.Command{ + Use: "diff ", + Short: "Show difference between running and desired configurations", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + log.Debug("running diff command") + svc := service.New(ceph.New(cephBinary), differ.New()) + prntr := printer.New(colorize) + return diffCmd.Diff(cmd.Context(), diffCmd.DiffConfig{ + Printer: prntr, + Service: svc, + SpecFile: args[0], + }) + }, +} + +var dumpCommand = &cobra.Command{ + Use: "dump", + Short: "Dump runtime configuration", + // dump without a child command — print the group's help. + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +var dumpCephConfigCommand = &cobra.Command{ + Use: "cephconfig", + Short: "dump Ceph runtime configuration", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + log.Debug("running dump cephconfig command") + svc := service.New(ceph.New(cephBinary), differ.New()) + prntr := printer.New(colorize) + return dumpCephConfigCmd.DumpCephConfig(cmd.Context(), dumpCephConfigCmd.DumpCephConfigConfig{ + Printer: prntr, + Service: svc, + }) + }, +} + +var dumpCephOSDConfigCommand = &cobra.Command{ + Use: "cephosdconfig", + Short: "dump Ceph OSD configuration", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + log.Debug("running dump cephosdconfig command") + svc := service.New(ceph.New(cephBinary), differ.New()) + prntr := printer.New(colorize) + return dumpCephOSDConfigCmd.DumpCephOSDConfig(cmd.Context(), dumpCephOSDConfigCmd.DumpCephOSDConfigConfig{ + Printer: prntr, + Service: svc, + }) + }, +} + +var healthcheckCommand = &cobra.Command{ + Use: "healthcheck", + Short: "Perform a cluster healthcheck and print report", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + svc := service.New(ceph.New(cephBinary), differ.New()) + prntr := printer.New(colorize) + return healthcheckCmd.Healthcheck(cmd.Context(), healthcheckCmd.HealthcheckConfig{ + Printer: prntr, + Service: svc, + }) + }, +} + +// applyStringEnv applies a string env value as the default for a flag, unless +// the flag was set explicitly by the user. +func applyStringEnv(cmd *cobra.Command, flagName, envName string) { + f := cmd.Flags().Lookup(flagName) + if f == nil || f.Changed { + return + } + if v, ok := os.LookupEnv(envName); ok { + _ = cmd.Flags().Set(flagName, v) + } +} + +// applyBoolEnv applies a truthy env value (1/true/yes/on) as the default for a +// bool flag, unless the flag was set explicitly by the user. +func applyBoolEnv(cmd *cobra.Command, flagName, envName string) { + f := cmd.Flags().Lookup(flagName) + if f == nil || f.Changed { + return + } + if v, ok := os.LookupEnv(envName); ok { + _ = cmd.Flags().Set(flagName, strconv.FormatBool(parseBool(v))) + } +} + +// parseBool treats an env string as truthy for bool flags. +func parseBool(v string) bool { + switch strings.ToLower(strings.TrimSpace(v)) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} diff --git a/cmd/cephctl/version.go b/cmd/cephctl/version.go new file mode 100644 index 0000000..dfc9f76 --- /dev/null +++ b/cmd/cephctl/version.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// appVersion and buildTimestamp are overridden at build time via ldflags: +// -X main.appVersion={{.Version}} -X main.buildTimestamp={{.Date}} +// see .goreleaser.yaml. +var ( + appVersion = "n/a (dev build)" + buildTimestamp = "undefined" +) + +var versionCommand = &cobra.Command{ + Use: "version", + Short: "Print version and exit", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf( + "%s v%s / built at %s\n", + os.Args[0], appVersion, buildTimestamp, + ) + return nil + }, +} diff --git a/go.mod b/go.mod index 828d6d3..cbe0a1a 100644 --- a/go.mod +++ b/go.mod @@ -3,25 +3,25 @@ module github.com/runityru/cephctl go 1.25.0 require ( - github.com/alecthomas/kingpin/v2 v2.4.0 github.com/creasty/defaults v1.8.0 github.com/fatih/color v1.19.0 github.com/pkg/errors v0.9.1 github.com/r3labs/diff/v3 v3.0.2 github.com/sirupsen/logrus v1.10.2 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.12.1 github.com/teran/go-ptr v1.1.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/sys v0.46.0 // indirect ) diff --git a/go.sum b/go.sum index ec578d9..93eac13 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,27 @@ -github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= -github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/r3labs/diff/v3 v3.0.2 h1:yVuxAY1V6MeM4+HNur92xkS39kB/N+cFi2hMkY06BbA= github.com/r3labs/diff/v3 v3.0.2/go.mod h1:Cy542hv0BAEmhDYWtGxXRQ4kqRsVIcEjG9gChUlTmkw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/teran/go-ptr v1.1.0 h1:c/5SPr+FB4zfmmKVB+KTCNKpgC/zWKBY5JHReL84lKU= @@ -37,14 +30,12 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= -github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=