From 848d35616fc95d58bdd7771c60f1a7c90c747a33 Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Fri, 20 May 2016 09:41:25 +0200 Subject: [PATCH] fleetctl: convert cli to cobra Use Cobra (github.com/spf13/cobra) instead of cli (github.com/codegangsta/cli), for better cmdline user interface. * Create a wrapper runWrapper() to be used for cobra, to wrap around a normal run*() function into a prototype for cobra.Command.Run(). It also sets a global variable cAPI for running a normal command. * remove unnecessary code for codegangsta/cli from fleetctl.go. Suggested-by: Jonathan Boulle Fixes: https://github.com/coreos/fleet/issues/1453 Supersedes https://github.com/coreos/fleet/pull/1570 --- fleetctl/cat.go | 24 +-- fleetctl/destroy.go | 31 ++- fleetctl/fdforward.go | 24 ++- fleetctl/fleetctl.go | 366 ++++++++++++++++++++++-------------- fleetctl/help.go | 129 +++++++++++++ fleetctl/journal.go | 58 +++--- fleetctl/list_machines.go | 70 +++---- fleetctl/list_unit_files.go | 50 ++--- fleetctl/list_units.go | 69 +++---- fleetctl/load.go | 44 ++--- fleetctl/ssh.go | 99 +++++----- fleetctl/start.go | 51 ++--- fleetctl/status.go | 33 ++-- fleetctl/stop.go | 37 ++-- fleetctl/submit.go | 43 ++--- fleetctl/unload.go | 30 ++- fleetctl/verify.go | 21 +-- fleetctl/version.go | 37 ++++ 18 files changed, 742 insertions(+), 474 deletions(-) create mode 100644 fleetctl/help.go create mode 100644 fleetctl/version.go diff --git a/fleetctl/cat.go b/fleetctl/cat.go index 82b578096..f7107e1bc 100644 --- a/fleetctl/cat.go +++ b/fleetctl/cat.go @@ -17,24 +17,24 @@ package main import ( "fmt" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/schema" ) -func NewCatCommand() cli.Command { - return cli.Command{ - Name: "cat", - Usage: "Output the contents of a submitted unit", - ArgsUsage: "UNIT", - Description: `Outputs the unit file that is currently loaded in the cluster. Useful to verify the correct version of a unit is running.`, - Action: makeActionWrapper(runCatUnit), - } +var cmdCat = &cobra.Command{ + Use: "cat UNIT", + Short: "Output the contents of a submitted unit", + Long: `Outputs the unit file that is currently loaded in the cluster. Useful to verify +the correct version of a unit is running.`, + Run: runWrapper(runCatUnit), +} + +func init() { + cmdFleet.AddCommand(cmdCat) } -func runCatUnit(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runCatUnit(cCmd *cobra.Command, args []string) (exit int) { if len(args) != 1 { stderr("One unit file must be provided") return 1 diff --git a/fleetctl/destroy.go b/fleetctl/destroy.go index b8bf9a441..0e6f1685a 100644 --- a/fleetctl/destroy.go +++ b/fleetctl/destroy.go @@ -17,35 +17,35 @@ package main import ( "time" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" "github.com/coreos/fleet/client" ) -func NewDestroyCommand() cli.Command { - return cli.Command{ - Name: "destroy", - Usage: "Destroy one or more units in the cluster", - ArgsUsage: "UNIT...", - Description: `Completely remove one or more running or submitted units from the cluster. +var cmdDestroy = &cobra.Command{ + Use: "destroy UNIT...", + Short: "Destroy one or more units in the cluster", + Long: `Completely remove one or more running or submitted units from the cluster. Instructs systemd on the host machine to stop the unit, deferring to systemd completely for any custom stop directives (i.e. ExecStop option in the unit file). Destroyed units are impossible to start unless re-submitted.`, - Action: makeActionWrapper(runDestroyUnits), - } + Run: runWrapper(runDestroyUnit), +} + +func init() { + cmdFleet.AddCommand(cmdDestroy) } -func runDestroyUnits(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runDestroyUnit(cCmd *cobra.Command, args []string) (exit int) { if len(args) == 0 { stderr("No units given") return 0 } - units, err := findUnits(args, cAPI) + units, err := findUnits(args) if err != nil { stderr("%v", err) return 1 @@ -63,10 +63,10 @@ func runDestroyUnits(c *cli.Context, cAPI client.API) (exit int) { continue } - if c.Bool("no-block") { - attempts := c.Int("block-attempts") + if sharedFlags.NoBlock { + attempts := sharedFlags.BlockAttempts retry := func() bool { - if c.Int("block-attempts") < 1 { + if sharedFlags.BlockAttempts < 1 { return true } attempts-- @@ -93,6 +93,5 @@ func runDestroyUnits(c *cli.Context, cAPI client.API) (exit int) { stdout("Destroyed %s", v.Name) } - return } diff --git a/fleetctl/fdforward.go b/fleetctl/fdforward.go index 55f8b255f..d12e52b17 100644 --- a/fleetctl/fdforward.go +++ b/fleetctl/fdforward.go @@ -19,23 +19,21 @@ import ( "net" "os" - "github.com/codegangsta/cli" - - "github.com/coreos/fleet/client" + "github.com/spf13/cobra" ) -func NewFDForwardCommand() cli.Command { - return cli.Command{ - Name: "fd-forward", - Usage: "Proxy stdin and stdout to a unix domain socket", - ArgsUsage: "SOCKET", - Description: `fleetctl utilizes fd-forward when --tunnel is used and --endpoint is a unix socket. This command is not intended to be called by users directly.`, - Action: makeActionWrapper(runFDForward), - } +var cmdFDForward = &cobra.Command{ + Use: "fd-forward SOCKET", + Short: "Proxy stdin and stdout to a unix domain socket", + Long: `fleetctl utilizes fd-forward when --tunnel is used and --endpoint is a unix socket. This command is not intended to be called by users directly.`, + Run: runWrapper(runFDForward), +} + +func init() { + cmdFleet.AddCommand(cmdFDForward) } -func runFDForward(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runFDForward(cCmd *cobra.Command, args []string) (exit int) { if len(args) != 1 { stderr("Provide a single argument") return 1 diff --git a/fleetctl/fleetctl.go b/fleetctl/fleetctl.go index 9fd30fd1a..18dd5e408 100644 --- a/fleetctl/fleetctl.go +++ b/fleetctl/fleetctl.go @@ -30,7 +30,7 @@ import ( "text/tabwriter" "time" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" etcd "github.com/coreos/etcd/client" @@ -68,19 +68,119 @@ recommended to upgrade fleetctl to prevent incompatibility issues. var ( out *tabwriter.Writer + // set of top-level commands + commands []*Command + // global API client used by commands cAPI client.API + // flags used by all commands + globalFlags = struct { + Debug bool + Version bool + Help bool + + ClientDriver string + ExperimentalAPI bool + Endpoint string + RequestTimeout float64 + + KeyFile string + CertFile string + CAFile string + + Tunnel string + KnownHostsFile string + StrictHostKeyChecking bool + SSHTimeout float64 + SSHUserName string + + EtcdKeyPrefix string + }{} + + // flags used by multiple commands + sharedFlags = struct { + Sign bool + Full bool + NoLegend bool + NoBlock bool + Replace bool + BlockAttempts int + Fields string + SSHPort int + }{} + // current command being executed currentCommand string // used to cache MachineStates machineStates map[string]*machine.MachineState + + cmdExitCode int ) +var cmdFleet = &cobra.Command{ + Use: cliName, + Short: cliDescription, + // SuggestFor: []string{"fleetctl"}, + Run: func(cCmd *cobra.Command, args []string) { + cCmd.HelpFunc()(cCmd, args) + }, +} + func init() { - out = new(tabwriter.Writer) - out.Init(os.Stdout, 0, 8, 1, '\t', 0) + out = getTabOutWithWriter(os.Stdout) + + // call this as early as possible to ensure we always have timestamps + // on fleetctl logs + log.EnableTimestamps() + + cobra.EnablePrefixMatching = true + + cmdFleet.PersistentFlags().BoolVar(&globalFlags.Help, "help", false, "Print usage information and exit") + cmdFleet.PersistentFlags().BoolVar(&globalFlags.Help, "h", false, "Print usage information and exit") + + cmdFleet.PersistentFlags().BoolVar(&globalFlags.Debug, "debug", false, "Print out more debug information to stderr") + cmdFleet.PersistentFlags().BoolVar(&globalFlags.Version, "version", false, "Print the version and exit") + cmdFleet.PersistentFlags().StringVar(&globalFlags.ClientDriver, "driver", clientDriverAPI, fmt.Sprintf("Adapter used to execute fleetctl commands. Options include %q and %q.", clientDriverAPI, clientDriverEtcd)) + cmdFleet.PersistentFlags().StringVar(&globalFlags.Endpoint, "endpoint", defaultEndpoint, fmt.Sprintf("Location of the fleet API if --driver=%s. Alternatively, if --driver=%s, location of the etcd API.", clientDriverAPI, clientDriverEtcd)) + cmdFleet.PersistentFlags().StringVar(&globalFlags.EtcdKeyPrefix, "etcd-key-prefix", registry.DefaultKeyPrefix, "Keyspace for fleet data in etcd (development use only!)") + + cmdFleet.PersistentFlags().StringVar(&globalFlags.KeyFile, "key-file", "", "Location of TLS key file used to secure communication with the fleet API or etcd") + cmdFleet.PersistentFlags().StringVar(&globalFlags.CertFile, "cert-file", "", "Location of TLS cert file used to secure communication with the fleet API or etcd") + cmdFleet.PersistentFlags().StringVar(&globalFlags.CAFile, "ca-file", "", "Location of TLS CA file used to secure communication with the fleet API or etcd") + + cmdFleet.PersistentFlags().StringVar(&globalFlags.KnownHostsFile, "known-hosts-file", ssh.DefaultKnownHostsFile, "File used to store remote machine fingerprints. Ignored if strict host key checking is disabled.") + cmdFleet.PersistentFlags().BoolVar(&globalFlags.StrictHostKeyChecking, "strict-host-key-checking", true, "Verify host keys presented by remote machines before initiating SSH connections.") + cmdFleet.PersistentFlags().Float64Var(&globalFlags.SSHTimeout, "ssh-timeout", 10.0, "Amount of time in seconds to allow for SSH connection initialization before failing.") + cmdFleet.PersistentFlags().StringVar(&globalFlags.Tunnel, "tunnel", "", "Establish an SSH tunnel through the provided address for communication with fleet and etcd.") + cmdFleet.PersistentFlags().Float64Var(&globalFlags.RequestTimeout, "request-timeout", 3.0, "Amount of time in seconds to allow a single request before considering it failed.") + cmdFleet.PersistentFlags().StringVar(&globalFlags.SSHUserName, "ssh-username", "core", "Username to use when connecting to CoreOS instance.") + + // deprecated flags + cmdFleet.PersistentFlags().BoolVar(&globalFlags.ExperimentalAPI, "experimental-api", true, "DEPRECATED: do not use this flag.") + cmdFleet.PersistentFlags().StringVar(&globalFlags.KeyFile, "etcd-keyfile", "", "DEPRECATED: do not use this flag.") + cmdFleet.PersistentFlags().StringVar(&globalFlags.CertFile, "etcd-certfile", "", "DEPRECATED: do not use this flag.") + cmdFleet.PersistentFlags().StringVar(&globalFlags.CAFile, "etcd-cafile", "", "DEPRECATED: do not use this flag.") +} + +type Command struct { + Name string // Name of the Command and the string to use to invoke it + Summary string // One-sentence summary of what the Command does + Usage string // Usage options/arguments + Description string // Detailed description of command + Flags flag.FlagSet // Set of flags associated with this command + + Run func(args []string) int // Run a command with the given arguments, return exit status + +} + +func getFlags(flagset *flag.FlagSet) (flags []*flag.Flag) { + flags = make([]*flag.Flag, 0) + flagset.VisitAll(func(f *flag.Flag) { + flags = append(flags, f) + }) + return } func maybeAddNewline(s string) string { @@ -113,70 +213,28 @@ func checkVersion(cReg registry.ClusterRegistry) (string, bool) { return "", true } -var ( - globalFlags = []cli.Flag{ - cli.BoolFlag{Name: "debug", Usage: "Print out more debug information to stderr"}, - cli.StringFlag{Name: "driver", Value: clientDriverAPI, Usage: fmt.Sprintf("Adapter used to execute fleetctl commands. Options include %q and %q.", clientDriverAPI, clientDriverEtcd)}, - cli.StringFlag{Name: "endpoint", Value: defaultEndpoint, Usage: fmt.Sprintf("Location of the fleet API if --driver=%s. Alternatively, if --driver=%s, location of the etcd API.", clientDriverAPI, clientDriverEtcd)}, - cli.StringFlag{Name: "etcd-key-prefix", Value: registry.DefaultKeyPrefix, Usage: "Keyspace for fleet data in etcd (development use only!)"}, - cli.StringFlag{Name: "key-file", Value: "", Usage: "Location of TLS key file used to secure communication with the fleet API or etcd"}, - cli.StringFlag{Name: "cert-file", Value: "", Usage: "Location of TLS cert file used to secure communication with the fleet API or etcd"}, - cli.StringFlag{Name: "ca-file", Value: "", Usage: "Location of TLS CA file used to secure communication with the fleet API or etcd"}, - cli.StringFlag{Name: "known-hosts-file", Value: ssh.DefaultKnownHostsFile, Usage: "File used to store remote machine fingerprints. Ignored if strict host key checking is disabled."}, - cli.BoolTFlag{Name: "strict-host-key-checking", Usage: "Verify host keys presented by remote machines before initiating SSH connections."}, - cli.DurationFlag{Name: "ssh-timeout", Value: 10 * time.Second, Usage: "Amount of time in seconds to allow for SSH connection initialization before failing."}, - cli.StringFlag{Name: "tunnel", Value: "", Usage: "Establish an SSH tunnel through the provided address for communication with fleet and etcd."}, - cli.DurationFlag{Name: "request-timeout", Value: 3 * time.Second, Usage: "Amount of time in seconds to allow a single request before considering it failed."}, - cli.StringFlag{Name: "ssh-username", Value: "core", Usage: "Username to use when connecting to CoreOS instance."}, - // deprecated flags - cli.BoolTFlag{Name: "experimental-api", Usage: "DEPRECATED"}, - cli.StringFlag{Name: "etcd-keyfile", Value: "", Usage: "DEPRECATED"}, - cli.StringFlag{Name: "etcd-certfile", Value: "", Usage: "DEPRECATED"}, - cli.StringFlag{Name: "etcd-cafile", Value: "", Usage: "DEPRECATED"}, - } - globalCommands = []cli.Command{ - NewCatCommand(), - NewDestroyCommand(), - NewFDForwardCommand(), - NewJournalCommand(), - NewListMachinesCommand(), - NewListUnitFilesCommand(), - NewListUnitsCommand(), - NewLoadUnitsCommand(), - NewSSHCommend(), - NewStartCommand(), - NewStatusCommand(), - NewStopUnitCommand(), - NewSubmitUnitCommand(), - NewUnloadUnitCommand(), - NewVerifyCommand(), - } -) - -func createApp() *cli.App { - app := cli.NewApp() - app.Name = "fleetctl" - app.Version = version.Version - app.Usage = "command-line interface to fleet." - - app.Flags = globalFlags - app.Commands = globalCommands - - return app -} - func main() { - app := createApp() + if globalFlags.Debug { + log.EnableDebug() + } // call this as early as possible to ensure we always have timestamps // on fleetctl logs log.EnableTimestamps() + if len(os.Args) == 1 { + cmdFleet.Help() + os.Exit(0) + } + + if os.Args[1] == "--version" || os.Args[1] == "-v" { + runVersion(cmdVersion, nil) + os.Exit(0) + } + // determine currentCommand. We only need this for --replace and its // functional tests, so just handle those for now in the switch... // "The rest" doesn't care about "currentCommand" - //stderr("%d command line arguments", len(os.Args)) - //stderr("%s", os.Args) if len(os.Args) > 1 { for i := 1; i < len(os.Args); i++ { switch os.Args[i] { @@ -190,22 +248,31 @@ func main() { continue } } - // stderr("First: %s", os.Args[1]) } - app.Run(os.Args) -} -func makeActionWrapper(action func(context *cli.Context, cAPI client.API) int) func(context *cli.Context) { - return func(c *cli.Context) { - if c.Bool("sign") { - stderr("WARNING: The signed/verified units feature is DEPRECATED and cannot be used.") - os.Exit(2) - } - cAPI := getClientAPI(c) - if ret := action(c, cAPI); ret != 0 { - os.Exit(ret) + if sharedFlags.Sign { + stderr("WARNING: The signed/verified units feature is DEPRECATED and cannot be used.") + os.Exit(2) + } + + // if --driver is not set, but --endpoint looks like an etcd + // server, set the driver to etcd + if globalFlags.Endpoint != "" && globalFlags.ClientDriver == "" { + if u, err := url.Parse(strings.Split(globalFlags.Endpoint, ",")[0]); err == nil { + if _, port, err := net.SplitHostPort(u.Host); err == nil && (port == "4001" || port == "2379") { + log.Debugf("Defaulting to --driver=%s as --endpoint appears to be etcd", clientDriverEtcd) + globalFlags.ClientDriver = clientDriverEtcd + } } } + + cmdFleet.SetUsageFunc(usageFunc) + cmdFleet.SetHelpTemplate(`{{.UsageString}}`) + + if err := cmdFleet.Execute(); err != nil { + stderr("cannot execute cmdFleet: %v", err) + } + os.Exit(cmdExitCode) } // getFlagsFromEnv parses all registered flags in the given flagset, @@ -230,9 +297,9 @@ func getFlagsFromEnv(prefix string, fs *flag.FlagSet) { }) } -func getClientAPI(c *cli.Context) client.API { +func getClientAPI(cCmd *cobra.Command) client.API { var err error - cAPI, err = getClient(c) + cAPI, err = getClient(cCmd) if err != nil { stderr("Unable to initialize client: %v", err) os.Exit(1) @@ -241,44 +308,35 @@ func getClientAPI(c *cli.Context) client.API { } // getClient initializes a client of fleet based on CLI flags -func getClient(c *cli.Context) (client.API, error) { - driverFlag := c.GlobalString("driver") - // if --driver is not set, but --endpoint looks like an etcd - // server, set the driver to etcd - if c.GlobalIsSet("endpoint") && !c.GlobalIsSet("driver") { - if u, err := url.Parse(strings.Split(c.GlobalString("endpoint"), ",")[0]); err == nil { - if _, port, err := net.SplitHostPort(u.Host); err == nil && (port == "4001" || port == "2379") { - log.Debugf("Defaulting to --driver=%s as --endpoint appears to be etcd", clientDriverEtcd) - driverFlag = clientDriverEtcd - } - } - } - - endpointFlag := c.GlobalString("endpoint") +func getClient(cCmd *cobra.Command) (client.API, error) { // The user explicitly set --experimental-api=false, so it trumps the // --driver flag. This behavior exists for backwards-compatibilty. - if !c.GlobalBool("experimental-api") { + experimentalAPI, _ := cmdFleet.PersistentFlags().GetBool("experimental-api") + endPoint, _ := cmdFleet.PersistentFlags().GetString("endpoint") + clientDriver, _ := cmdFleet.PersistentFlags().GetString("driver") + if !experimentalAPI { // Additionally, if the user set --experimental-api=false and did // not change the value of --endpoint, they likely want to use the // old default value. - if endpointFlag == defaultEndpoint { - endpointFlag = "http://127.0.0.1:2379,http://127.0.0.1:4001" + if endPoint == defaultEndpoint { + endPoint = "http://127.0.0.1:2379,http://127.0.0.1:4001" } - return getRegistryClient(c) + return getRegistryClient(cCmd) } - switch driverFlag { + switch clientDriver { case clientDriverAPI: - return getHTTPClient(c, endpointFlag) + return getHTTPClient(cCmd) case clientDriverEtcd: - return getRegistryClient(c) + return getRegistryClient(cCmd) } - return nil, fmt.Errorf("unrecognized driver %q", driverFlag) + return nil, fmt.Errorf("unrecognized driver %q", clientDriver) } -func getHTTPClient(c *cli.Context, endpointFlag string) (client.API, error) { - endpoints := strings.Split(endpointFlag, ",") +func getHTTPClient(cCmd *cobra.Command) (client.API, error) { + endPoint, _ := cmdFleet.PersistentFlags().GetString("endpoint") + endpoints := strings.Split(endPoint, ",") if len(endpoints) > 1 { log.Warningf("multiple endpoints provided but only the first (%s) is used", endpoints[0]) } @@ -292,14 +350,15 @@ func getHTTPClient(c *cli.Context, endpointFlag string) (client.API, error) { return nil, errors.New("URL scheme undefined") } - tun := getTunnelFlag(c) + tun := getTunnelFlag(cCmd) tunneling := tun != "" dialUnix := ep.Scheme == "unix" || ep.Scheme == "file" + SSHUserName, _ := cmdFleet.PersistentFlags().GetString("ssh-username") tunnelFunc := net.Dial if tunneling { - sshClient, err := ssh.NewSSHClient(c.GlobalString("ssh-username"), tun, getChecker(c), true, getSSHTimeoutFlag(c)) + sshClient, err := ssh.NewSSHClient(SSHUserName, tun, getChecker(cCmd), true, getSSHTimeoutFlag(cCmd)) if err != nil { return nil, fmt.Errorf("failed initializing SSH client: %v", err) } @@ -346,7 +405,10 @@ func getHTTPClient(c *cli.Context, endpointFlag string) (client.API, error) { ep.Host = "domain-sock" } - tlsConfig, err := pkg.ReadTLSConfigFiles(c.GlobalString("ca-file"), c.GlobalString("cert-file"), c.GlobalString("key-file")) + CAFile, _ := cmdFleet.PersistentFlags().GetString("ca-file") + CertFile, _ := cmdFleet.PersistentFlags().GetString("cert-file") + KeyFile, _ := cmdFleet.PersistentFlags().GetString("key-file") + tlsConfig, err := pkg.ReadTLSConfigFiles(CAFile, CertFile, KeyFile) if err != nil { return nil, err } @@ -365,11 +427,12 @@ func getHTTPClient(c *cli.Context, endpointFlag string) (client.API, error) { return client.NewHTTPClient(&hc, *ep) } -func getRegistryClient(c *cli.Context) (client.API, error) { +func getRegistryClient(cCmd *cobra.Command) (client.API, error) { var dial func(string, string) (net.Conn, error) - tun := getTunnelFlag(c) + SSHUserName, _ := cmdFleet.PersistentFlags().GetString("ssh-username") + tun := getTunnelFlag(cCmd) if tun != "" { - sshClient, err := ssh.NewSSHClient(c.GlobalString("ssh-username"), tun, getChecker(c), false, getSSHTimeoutFlag(c)) + sshClient, err := ssh.NewSSHClient(SSHUserName, tun, getChecker(cCmd), false, getSSHTimeoutFlag(cCmd)) if err != nil { return nil, fmt.Errorf("failed initializing SSH client: %v", err) } @@ -383,7 +446,10 @@ func getRegistryClient(c *cli.Context) (client.API, error) { } } - tlsConfig, err := pkg.ReadTLSConfigFiles(c.GlobalString("ca-file"), c.GlobalString("cert-file"), c.GlobalString("key-file")) + CAFile, _ := cmdFleet.PersistentFlags().GetString("ca-file") + CertFile, _ := cmdFleet.PersistentFlags().GetString("cert-file") + KeyFile, _ := cmdFleet.PersistentFlags().GetString("key-file") + tlsConfig, err := pkg.ReadTLSConfigFiles(CAFile, CertFile, KeyFile) if err != nil { return nil, err } @@ -393,10 +459,11 @@ func getRegistryClient(c *cli.Context) (client.API, error) { TLSClientConfig: tlsConfig, } + endPoint, _ := cmdFleet.PersistentFlags().GetString("endpoint") eCfg := etcd.Config{ - Endpoints: strings.Split(c.GlobalString("endpoint"), ","), + Endpoints: strings.Split(endPoint, ","), Transport: trans, - HeaderTimeoutPerRequest: getRequestTimeoutFlag(), + HeaderTimeoutPerRequest: getRequestTimeoutFlag(cCmd), } eClient, err := etcd.New(eCfg) @@ -404,8 +471,9 @@ func getRegistryClient(c *cli.Context) (client.API, error) { return nil, err } + etcdKeyPrefix, _ := cmdFleet.PersistentFlags().GetString("etcd-key-prefix") kAPI := etcd.NewKeysAPI(eClient) - reg := registry.NewEtcdRegistry(kAPI, c.GlobalString("etcd-key-prefix")) + reg := registry.NewEtcdRegistry(kAPI, etcdKeyPrefix) if msg, ok := checkVersion(reg); !ok { stderr(msg) @@ -415,12 +483,14 @@ func getRegistryClient(c *cli.Context) (client.API, error) { } // getChecker creates and returns a HostKeyChecker, or nil if any error is encountered -func getChecker(c *cli.Context) *ssh.HostKeyChecker { - if !c.GlobalBool("strict-host-key-checking") { +func getChecker(cCmd *cobra.Command) *ssh.HostKeyChecker { + strictHostKeyChecking, _ := cmdFleet.PersistentFlags().GetBool("strict-host-key-checking") + if !strictHostKeyChecking { return nil } - keyFile := ssh.NewHostKeyFile(c.GlobalString("known-hosts-file")) + knownHostsFile, _ := cmdFleet.PersistentFlags().GetString("known-hosts-file") + keyFile := ssh.NewHostKeyFile(knownHostsFile) return ssh.NewHostKeyChecker(keyFile) } @@ -431,7 +501,7 @@ func getChecker(c *cli.Context) *ssh.HostKeyChecker { // tries to get the template configuration either from the registry or // the local disk. // It returns a UnitFile configuration or nil; and any error ecountered -func getUnitFile(file string, c *cli.Context) (*unit.UnitFile, error) { +func getUnitFile(cCmd *cobra.Command, file string) (*unit.UnitFile, error) { var uf *unit.UnitFile name := unitNameMangle(file) @@ -459,7 +529,7 @@ func getUnitFile(file string, c *cli.Context) (*unit.UnitFile, error) { // If we found a template unit, later we create a // near-identical instance unit in the Registry - same // unit file as the template, but different name - uf, err = getUnitFileFromTemplate(info, file, c) + uf, err = getUnitFileFromTemplate(cCmd, info, file) if err != nil { return nil, fmt.Errorf("failed getting Unit(%s) from template: %v", file, err) } @@ -487,7 +557,7 @@ func getUnitFromFile(file string) (*unit.UnitFile, error) { // is either in the registry or on the file system // It takes two arguments, the template information and the unit file name // It returns the Unit or nil; and any error encountered -func getUnitFileFromTemplate(uni *unit.UnitNameInfo, fileName string, c *cli.Context) (*unit.UnitFile, error) { +func getUnitFileFromTemplate(cCmd *cobra.Command, uni *unit.UnitNameInfo, fileName string) (*unit.UnitFile, error) { var uf *unit.UnitFile tmpl, err := cAPI.Unit(uni.Template) @@ -496,7 +566,7 @@ func getUnitFileFromTemplate(uni *unit.UnitNameInfo, fileName string, c *cli.Con } if tmpl != nil { - isLocalUnitDifferent(fileName, tmpl, false, c) + isLocalUnitDifferent(cCmd, fileName, tmpl, false) uf = schema.MapSchemaUnitOptionsToUnitFile(tmpl.Options) log.Debugf("Template Unit(%s) found in registry", uni.Template) } else { @@ -516,20 +586,22 @@ func getUnitFileFromTemplate(uni *unit.UnitNameInfo, fileName string, c *cli.Con return uf, nil } -func getTunnelFlag(c *cli.Context) string { - tun := c.GlobalString("tunnel") +func getTunnelFlag(cCmd *cobra.Command) string { + tun, _ := cmdFleet.PersistentFlags().GetString("tunnel") if tun != "" && !strings.Contains(tun, ":") { tun += ":22" } return tun } -func getSSHTimeoutFlag(c *cli.Context) time.Duration { - return c.GlobalDuration("ssh-timeout") +func getSSHTimeoutFlag(cCmd *cobra.Command) time.Duration { + sshTimeout, _ := cmdFleet.PersistentFlags().GetFloat64("ssh-timeout") + return time.Duration(sshTimeout*1000) * time.Millisecond } -func getRequestTimeoutFlag(c *cli.Context) time.Duration { - return c.GlobalDuration("request-timeout") +func getRequestTimeoutFlag(cCmd *cobra.Command) time.Duration { + reqTimeout, _ := cmdFleet.PersistentFlags().GetFloat64("request-timeout") + return time.Duration(reqTimeout*1000) * time.Millisecond } func machineIDLegend(ms machine.MachineState, full bool) string { @@ -648,7 +720,7 @@ func checkReplaceUnitState(unit *schema.Unit) (int, error) { // It takes a unit file path as a parameter. // It returns 0 on success and if the unit should be created, 1 if the // unit should not be created; and any error encountered. -func checkUnitCreation(arg string, c *cli.Context) (int, error) { +func checkUnitCreation(cCmd *cobra.Command, arg string) (int, error) { name := unitNameMangle(arg) // First, check if there already exists a Unit by the given name in the Registry @@ -657,21 +729,22 @@ func checkUnitCreation(arg string, c *cli.Context) (int, error) { return 1, fmt.Errorf("error retrieving Unit(%s) from Registry: %v", name, err) } + replace, _ := cCmd.Flags().GetBool("replace") + // check if the unit is running if unit == nil { - if c.Bool("replace") { + if replace { log.Debugf("Unit(%s) was not found in Registry", name) } // Create a new unit return 0, nil } - // if sharedFlags.Replace is not set then we warn in case - // the units differ - different, err := isLocalUnitDifferent(arg, unit, false, c) + // if replace is not set then we warn in case the units differ + different, err := isLocalUnitDifferent(cCmd, arg, unit, false) - // if sharedFlags.Replace is set then we fail for errors - if c.Bool("replace") { + // if replace is set then we fail for errors + if replace { if err != nil { return 1, err } else if different { @@ -696,15 +769,15 @@ func checkUnitCreation(arg string, c *cli.Context) (int, error) { // Any error encountered during these steps is returned immediately (i.e. // subsequent Jobs are not acted on). An error is also returned if none of the // above conditions match a given Job. -func lazyCreateUnits(c *cli.Context) error { - args := c.Args() +func lazyCreateUnits(cCmd *cobra.Command, args []string) error { errchan := make(chan error) + blockAttempts, _ := cCmd.Flags().GetInt("block-attempts") var wg sync.WaitGroup for _, arg := range args { arg = maybeAppendDefaultUnitType(arg) name := unitNameMangle(arg) - ret, err := checkUnitCreation(arg, c) + ret, err := checkUnitCreation(cCmd, arg) if err != nil { return err } else if ret != 0 { @@ -714,7 +787,7 @@ func lazyCreateUnits(c *cli.Context) error { // Assume that the name references a local unit file on // disk or if it is an instance unit and if so get its // corresponding unit - uf, err := getUnitFile(arg, c) + uf, err := getUnitFile(cCmd, arg) if err != nil { return err } @@ -725,7 +798,7 @@ func lazyCreateUnits(c *cli.Context) error { } wg.Add(1) - go checkUnitState(name, job.JobStateInactive, c.Int("block-attempts"), os.Stdout, &wg, errchan) + go checkUnitState(name, job.JobStateInactive, blockAttempts, os.Stdout, &wg, errchan) } go func() { @@ -777,11 +850,13 @@ func matchLocalFileAndUnit(file string, su *schema.Unit) (bool, error) { // happen. // Returns true if the local Unit on file system is different from the // one provided, false otherwise; and any error encountered. -func isLocalUnitDifferent(file string, su *schema.Unit, fatal bool, c *cli.Context) (bool, error) { +func isLocalUnitDifferent(cCmd *cobra.Command, file string, su *schema.Unit, fatal bool) (bool, error) { + replace, _ := cCmd.Flags().GetBool("replace") + result, err := matchLocalFileAndUnit(file, su) if err == nil { // Warn in case unit differs from local file - if result == false && !c.Bool("replace") { + if result == false && !replace { stderr("WARNING: Unit %s in registry differs from local unit file %s. Add --replace to override.", su.Name, file) } return !result, nil @@ -800,7 +875,7 @@ func isLocalUnitDifferent(file string, su *schema.Unit, fatal bool, c *cli.Conte result, err = matchLocalFileAndUnit(templFile, su) if err == nil { // Warn in case unit differs from local template unit file - if result == false && !c.Bool("replace") { + if result == false && !replace { stderr("WARNING: Unit %s in registry differs from local template unit file %s. Add --replace to override.", su.Name, file) } return !result, nil @@ -857,15 +932,15 @@ func setTargetStateOfUnits(units []string, state job.JobState) ([]*schema.Unit, // It returns a negative value which means do not block, if zero is // returned then it means try forever, and if a positive value is // returned then try up to that value -func getBlockAttempts(c *cli.Context) int { +func getBlockAttempts(cCmd *cobra.Command) int { // By default we wait forever var attempts int = 0 - if c.Int("block-attempts") > 0 { - attempts = c.Int("block-attempts") + if sharedFlags.BlockAttempts > 0 { + attempts = sharedFlags.BlockAttempts } - if c.Bool("no-block") { + if sharedFlags.NoBlock { attempts = -1 } @@ -1035,3 +1110,18 @@ func suToGlobal(su schema.Unit) bool { } return u.IsGlobal() } + +// runWrapper returns a func(cCmd *cobra.Command, args []string) that +// internally will add command function return code, to be able to used for +// cobra.Command.Run(). +// Note that cAPI must be set before calling cf(), to be able to distinguish +// different contexts, i.e. a normal cmdline (cAPI) vs. unit test (fakeAPI). +// So the setting cAPI in runWrapper() has nothing to do with the unit test +// context. In case of unit tests, cAPI will be set to fakeAPI before calling +// each run() function, which won't reach runWrapper at all. +func runWrapper(cf func(cCmd *cobra.Command, args []string) (exit int)) func(cCmd *cobra.Command, args []string) { + return func(cCmd *cobra.Command, args []string) { + cAPI = getClientAPI(cCmd) + cmdExitCode = cf(cCmd, args) + } +} diff --git a/fleetctl/help.go b/fleetctl/help.go new file mode 100644 index 000000000..204ab33b0 --- /dev/null +++ b/fleetctl/help.go @@ -0,0 +1,129 @@ +// Copyright 2016 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "io" + "os" + "strings" + "text/tabwriter" + "text/template" + + "github.com/coreos/fleet/version" + "github.com/spf13/cobra" +) + +var ( + commandUsageTemplate *template.Template + templFuncs = template.FuncMap{ + "descToLines": func(s string) []string { + // trim leading/trailing whitespace and split into slice of lines + return strings.Split(strings.Trim(s, "\n\t "), "\n") + }, + "cmdName": func(cCmd *cobra.Command, startCmd *cobra.Command) string { + parts := []string{cCmd.Name()} + for cCmd.HasParent() && cCmd.Parent().Name() != startCmd.Name() { + cCmd = cCmd.Parent() + parts = append([]string{cCmd.Name()}, parts...) + } + return strings.Join(parts, " ") + }, + } +) + +func init() { + commandUsage := ` +{{ $cmd := .Cmd }}\ +{{ $cmdname := cmdName .Cmd .Cmd.Root }}\ +NAME: +{{ if not .Cmd.HasParent }}\ +{{printf "\t%s - %s" .Cmd.Name .Cmd.Short}} +{{else}}\ +{{printf "\t%s - %s" $cmdname .Cmd.Short}} +{{end}}\ + +USAGE: +{{printf "\t%s" .Cmd.UseLine}} +{{ if not .Cmd.HasParent }}\ + +VERSION: +{{printf "\t%s" .Version}} +{{end}}\ +{{if .Cmd.HasSubCommands}}\ + +COMMANDS: +{{range .SubCommands}}\ +{{ $cmdname := cmdName . $cmd }}\ +{{ if .Runnable }}\ +{{printf "\t%s\t%s" $cmdname .Short}} +{{end}}\ +{{end}}\ +{{end}}\ +{{ if .Cmd.Long }}\ + +DESCRIPTION: +{{range $line := descToLines .Cmd.Long}}{{printf "\t%s" $line}} +{{end}}\ +{{end}}\ +{{if .Cmd.HasLocalFlags}}\ + +OPTIONS: +{{.LocalFlags}}\ +{{end}}\ +{{if .Cmd.HasInheritedFlags}}\ + +GLOBAL OPTIONS: +{{.GlobalFlags}}\ +{{end}} +`[1:] + + commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(strings.Replace(commandUsage, "\\\n", "", -1))) +} + +func getSubCommands(cCmd *cobra.Command) []*cobra.Command { + var subCommands []*cobra.Command + for _, subCmd := range cCmd.Commands() { + subCommands = append(subCommands, subCmd) + subCommands = append(subCommands, getSubCommands(subCmd)...) + } + return subCommands +} + +func usageFunc(cCmd *cobra.Command) error { + subCommands := getSubCommands(cCmd) + tabOut := getTabOutWithWriter(os.Stdout) + commandUsageTemplate.Execute(tabOut, struct { + Cmd *cobra.Command + LocalFlags string + GlobalFlags string + SubCommands []*cobra.Command + Version string + }{ + cCmd, + cCmd.LocalFlags().FlagUsages(), + cCmd.InheritedFlags().FlagUsages(), + subCommands, + version.Version, + }) + tabOut.Flush() + return nil +} + +func getTabOutWithWriter(writer io.Writer) *tabwriter.Writer { + aTabOut := new(tabwriter.Writer) + aTabOut.Init(writer, 0, 8, 1, '\t', 0) + + return aTabOut +} diff --git a/fleetctl/journal.go b/fleetctl/journal.go index b3fcca522..d48e24620 100644 --- a/fleetctl/journal.go +++ b/fleetctl/journal.go @@ -17,39 +17,45 @@ package main import ( "strconv" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/job" ) -func NewJournalCommand() cli.Command { - return cli.Command{ - Name: "journal", - Usage: "Print the journal of a unit in the cluster to stdout", - ArgsUsage: "[--lines=N] [--ssh-port=N] [-f|--follow] [--output=STRING] ", - Action: makeActionWrapper(runJournal), - Description: `Outputs the journal of a unit by connecting to the machine that the unit occupies. +var ( + flagLines int + flagFollow bool + flagSudo bool + flagOutput string +) + +var cmdJournal = &cobra.Command{ + Use: "journal [--lines=N] [--ssh-port=N] [-f|--follow] [--output=STRING] ", + Short: "Print the journal of a unit in the cluster to stdout", + Long: `Outputs the journal of a unit by connecting to the machine that the unit occupies. Read the last 10 lines: - fleetctl journal foo.service +fleetctl journal foo.service Read the last 100 lines: - fleetctl journal --lines 100 foo.service +fleetctl journal --lines 100 foo.service This command does not work with global units.`, - Flags: []cli.Flag{ - cli.IntFlag{Name: "lines", Value: 10, Usage: "Number of recent log lines to return"}, - cli.BoolFlag{Name: "follow, f", Usage: "Continuously print new entries as they are appended to the journal."}, - cli.IntFlag{Name: "ssh-port", Value: 22, Usage: "Connect to remote hosts over SSH using this TCP port"}, - cli.BoolFlag{Name: "sudo", Usage: "Execute journal command with sudo"}, - cli.StringFlag{Name: "output", Value: "short", Usage: "Output mode. This will be passed unaltered to journalctl on the remote host, and hence supports the same modes as that command."}, - }, - } + Run: runWrapper(runJournal), +} + +func init() { + cmdFleet.AddCommand(cmdJournal) + + cmdJournal.Flags().IntVar(&flagLines, "lines", 10, "Number of recent log lines to return") + cmdJournal.Flags().BoolVar(&flagFollow, "follow", false, "Continuously print new entries as they are appended to the journal.") + cmdJournal.Flags().BoolVar(&flagFollow, "f", false, "Shorthand for --follow") + cmdJournal.Flags().IntVar(&sharedFlags.SSHPort, "ssh-port", 22, "Connect to remote hosts over SSH using this TCP port") + cmdJournal.Flags().BoolVar(&flagSudo, "sudo", false, "Execute journal command with sudo") + cmdJournal.Flags().StringVar(&flagOutput, "output", "short", "Output mode. This will be passed unaltered to journalctl on the remote host, and hence supports the same modes as that command.") } -func runJournal(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runJournal(cCmd *cobra.Command, args []string) (exit int) { if len(args) != 1 { stderr("One unit file must be provided.") return 1 @@ -71,15 +77,17 @@ func runJournal(c *cli.Context, cAPI client.API) (exit int) { return 1 } - cmd := []string{"journalctl", "--unit", name, "--no-pager", "-n", strconv.Itoa(c.Int("lines")), "--output", c.String("output")} + lines, _ := cCmd.Flags().GetInt("lines") + cmd := []string{"journalctl", "--unit", name, "--no-pager", "-n", strconv.Itoa(lines), "--output", flagOutput} - if c.Bool("sudo") { + if flagSudo { cmd = append([]string{"sudo"}, cmd...) } - if c.Bool("follow") { + if flagFollow { cmd = append(cmd, "-f") } - return runCommand(c, u.MachineID, cmd[0], cmd[1:]...) + exit = runCommand(cCmd, u.MachineID, cmd[0], cmd[1:]...) + return } diff --git a/fleetctl/list_machines.go b/fleetctl/list_machines.go index 8b83bf296..2d8ebbded 100644 --- a/fleetctl/list_machines.go +++ b/fleetctl/list_machines.go @@ -19,37 +19,18 @@ import ( "sort" "strings" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/machine" ) -func NewListMachinesCommand() cli.Command { - return cli.Command{ - Name: "list-machines", - Usage: "Enumerate the current hosts in the cluster", - Description: `Lists all active machines within the cluster. Previously active machines will not appear in this list. - -For easily parsable output, you can remove the column headers: - fleetctl list-machines --no-legend - -Output the list without truncation: - fleetctl list-machines --full`, - ArgsUsage: "[-l|--full] [--no-legend]", - Action: makeActionWrapper(runListMachines), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "full, l", Usage: "Output the list without truncation"}, - cli.BoolFlag{Name: "no-legend", Usage: "Remove the column headers"}, - cli.StringFlag{Name: "fields", Value: defaultListMachinesFields, Usage: fmt.Sprintf("Columns to print for each Machine. Valid fields are %s", defaultListMachinesFields)}, - }, - } -} +const ( + defaultListMachinesFields = "machine,ip,metadata" +) var ( - //listMachinesFieldsFlag string - // Update defaultListMachinesFields if you add a new field here - listMachinesFields = map[string]machineToField{ + listMachinesFieldsFlag string + listMachinesFields = map[string]machineToField{ "machine": func(ms *machine.MachineState, full bool) string { return machineIDLegend(*ms, full) }, @@ -68,14 +49,31 @@ var ( } ) -const ( - defaultListMachinesFields = "machine,ip,metadata" -) - type machineToField func(ms *machine.MachineState, full bool) string -func runListMachines(c *cli.Context, cAPI client.API) (exit int) { - listMachinesFieldsFlag := c.String("fields") +var cmdListMachines = &cobra.Command{ + Use: "list-machines [-l|--full] [--no-legend]", + Short: "Enumerate the current hosts in the cluster", + Long: `Lists all active machines within the cluster. Previously active machines will not appear in this list. + +For easily parsable output, you can remove the column headers: +fleetctl list-machines --no-legend + +Output the list without truncation: +fleetctl list-machines --full`, + Run: runWrapper(runListMachines), +} + +func init() { + cmdFleet.AddCommand(cmdListMachines) + + cmdListMachines.Flags().BoolVar(&sharedFlags.Full, "full", false, "Do not ellipsize fields on output") + cmdListMachines.Flags().BoolVar(&sharedFlags.Full, "l", false, "Shorthand for --full") + cmdListMachines.Flags().BoolVar(&sharedFlags.NoLegend, "no-legend", false, "Do not print a legend (column headers)") + cmdListMachines.Flags().StringVar(&listMachinesFieldsFlag, "fields", defaultListMachinesFields, fmt.Sprintf("Columns to print for each Machine. Valid fields are %q", strings.Join(machineToFieldKeys(listMachinesFields), ","))) +} + +func runListMachines(cCmd *cobra.Command, args []string) (exit int) { if listMachinesFieldsFlag == "" { stderr("Must define output format") return 1 @@ -95,22 +93,24 @@ func runListMachines(c *cli.Context, cAPI client.API) (exit int) { return 1 } - if !c.Bool("no-legend") { + noLegend, _ := cCmd.Flags().GetBool("no-legend") + if !noLegend { fmt.Fprintln(out, strings.ToUpper(strings.Join(cols, "\t"))) } + full, _ := cCmd.Flags().GetBool("full") for _, ms := range machines { ms := ms var f []string - for _, col := range cols { - f = append(f, listMachinesFields[col](&ms, c.Bool("full"))) + for _, c := range cols { + f = append(f, listMachinesFields[c](&ms, full)) } fmt.Fprintln(out, strings.Join(f, "\t")) } out.Flush() - return + return 0 } func formatMetadata(metadata map[string]string) string { diff --git a/fleetctl/list_unit_files.go b/fleetctl/list_unit_files.go index 1c9d2bc03..b23af4286 100644 --- a/fleetctl/list_unit_files.go +++ b/fleetctl/list_unit_files.go @@ -20,9 +20,8 @@ import ( "strconv" "strings" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/machine" "github.com/coreos/fleet/schema" ) @@ -46,23 +45,9 @@ func mapTargetField(u schema.Unit, full bool) string { return machineFullLegend(*ms, full) } -func NewListUnitFilesCommand() cli.Command { - return cli.Command{ - Name: "list-unit-files", - Usage: "List the units that exist in the cluster.", - ArgsUsage: "[--fields]", - Description: "Lists all unit files that exist in the cluster (whether or not they are loaded onto a machine)", - Action: makeActionWrapper(runListUnitFiles), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "full", Usage: "Do not ellipsize fields on output"}, - cli.BoolFlag{Name: "no-legend", Usage: "Do not print a legend (column headers)"}, - cli.StringFlag{Name: "fields", Value: defaultListUnitFilesFields, Usage: fmt.Sprintf("Columns to print for each Unit file. Valid fields are %q", strings.Join(unitToFieldKeys(listUnitFilesFields), ","))}, - }, - } -} - var ( - listUnitFilesFields = map[string]unitToField{ + listUnitFilesFieldsFlag string + listUnitFilesFields = map[string]unitToField{ "unit": func(u schema.Unit, full bool) string { return u.Name }, @@ -103,8 +88,22 @@ var ( type unitToField func(u schema.Unit, full bool) string -func runListUnitFiles(c *cli.Context, cAPI client.API) (exit int) { - listUnitFilesFieldsFlag := c.String("fields") +var cmdListUnitFiles = &cobra.Command{ + Use: "list-unit-files [--fields]", + Short: "List the units that exist in the cluster.", + Long: `Lists all unit files that exist in the cluster (whether or not they are loaded onto a machine).`, + Run: runWrapper(runListUnitFiles), +} + +func init() { + cmdFleet.AddCommand(cmdListUnitFiles) + + cmdListUnitFiles.Flags().BoolVar(&sharedFlags.Full, "full", false, "Do not ellipsize fields on output") + cmdListUnitFiles.Flags().BoolVar(&sharedFlags.NoLegend, "no-legend", false, "Do not print a legend (column headers)") + cmdListUnitFiles.Flags().StringVar(&listUnitFilesFieldsFlag, "fields", defaultListUnitFilesFields, fmt.Sprintf("Columns to print for each Unit file. Valid fields are %q", strings.Join(unitToFieldKeys(listUnitFilesFields), ","))) +} + +func runListUnitFiles(cCmd *cobra.Command, args []string) (exit int) { if listUnitFilesFieldsFlag == "" { stderr("Must define output format") return 1 @@ -127,21 +126,22 @@ func runListUnitFiles(c *cli.Context, cAPI client.API) (exit int) { return 1 } - if !c.Bool("no-legend") { + noLegend, _ := cCmd.Flags().GetBool("no-legend") + if !noLegend { fmt.Fprintln(out, strings.ToUpper(strings.Join(cols, "\t"))) } + full, _ := cCmd.Flags().GetBool("full") for _, u := range units { var f []string - for _, col := range cols { - f = append(f, listUnitFilesFields[col](*u, c.Bool("full"))) + for _, c := range cols { + f = append(f, listUnitFilesFields[c](*u, full)) } fmt.Fprintln(out, strings.Join(f, "\t")) } out.Flush() - - return + return 0 } func unitToFieldKeys(m map[string]unitToField) (keys []string) { diff --git a/fleetctl/list_units.go b/fleetctl/list_units.go index d82b85c60..7a299feb1 100644 --- a/fleetctl/list_units.go +++ b/fleetctl/list_units.go @@ -19,9 +19,8 @@ import ( "sort" "strings" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/machine" "github.com/coreos/fleet/schema" ) @@ -30,32 +29,9 @@ const ( defaultListUnitsFields = "unit,machine,active,sub" ) -func NewListUnitsCommand() cli.Command { - return cli.Command{ - Name: "list-units", - Usage: "List the current state of units in the cluster", - ArgsUsage: "[--no-legend] [-l|--full] [--fields]", - Description: `Lists the state of all units in the cluster loaded onto a machine. - -For easily parsable output, you can remove the column headers: - fleetctl list-units --no-legend - -Output the list without ellipses: - fleetctl list-units --full - -Or, choose the columns to display: - fleetctl list-units --fields=unit,machine`, - Action: makeActionWrapper(runListUnits), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "full, l", Usage: "Do not ellipsize fields on output"}, - cli.BoolFlag{Name: "no-legend", Usage: "Do not print a legend (column headers)"}, - cli.StringFlag{Name: "fields", Value: defaultListUnitsFields, Usage: fmt.Sprintf("Columns to print for each Unit. Valid fields are %q", strings.Join(usToFieldKeys(listUnitsFields), ","))}, - }, - } -} - var ( - listUnitsFields = map[string]usToField{ + listUnitsFieldsFlag string + listUnitsFields = map[string]usToField{ "unit": func(us *schema.UnitState, full bool) string { if us == nil { return "-" @@ -104,8 +80,32 @@ var ( type usToField func(us *schema.UnitState, full bool) string -func runListUnits(c *cli.Context, cAPI client.API) (exit int) { - listUnitsFieldsFlag := c.String("fields") +var cmdListUnits = &cobra.Command{ + Use: "list-units [--no-legend] [-l|--full] [--fields]", + Short: "List the current state of units in the cluster", + Long: `Lists the state of all units in the cluster loaded onto a machine. + +For easily parsable output, you can remove the column headers: +fleetctl list-units --no-legend + +Output the list without ellipses: +fleetctl list-units --full + +Or, choose the columns to display: +fleetctl list-units --fields=unit,machine`, + Run: runWrapper(runListUnits), +} + +func init() { + cmdFleet.AddCommand(cmdListUnits) + + cmdListUnits.Flags().BoolVar(&sharedFlags.Full, "full", false, "Do not ellipsize fields on output") + cmdListUnits.Flags().BoolVar(&sharedFlags.Full, "l", false, "Shorthand for --full") + cmdListUnits.Flags().BoolVar(&sharedFlags.NoLegend, "no-legend", false, "Do not print a legend (column headers)") + cmdListUnits.Flags().StringVar(&listUnitsFieldsFlag, "fields", defaultListUnitsFields, fmt.Sprintf("Columns to print for each Unit. Valid fields are %q", strings.Join(usToFieldKeys(listUnitsFields), ","))) +} + +func runListUnits(cCmd *cobra.Command, args []string) (exit int) { if listUnitsFieldsFlag == "" { stderr("Must define output format") return 1 @@ -125,21 +125,22 @@ func runListUnits(c *cli.Context, cAPI client.API) (exit int) { return 1 } - if !c.Bool("no-legend") { + noLegend, _ := cCmd.Flags().GetBool("no-legend") + if !noLegend { fmt.Fprintln(out, strings.ToUpper(strings.Join(cols, "\t"))) } + full, _ := cCmd.Flags().GetBool("full") for _, us := range states { var f []string - for _, col := range cols { - f = append(f, listUnitsFields[col](us, c.Bool("full"))) + for _, c := range cols { + f = append(f, listUnitsFields[c](us, full)) } fmt.Fprintln(out, strings.Join(f, "\t")) } out.Flush() - - return + return 0 } func usToFieldKeys(m map[string]usToField) (keys []string) { diff --git a/fleetctl/load.go b/fleetctl/load.go index 977825da4..e14dc8234 100644 --- a/fleetctl/load.go +++ b/fleetctl/load.go @@ -17,18 +17,15 @@ package main import ( "os" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/job" ) -func NewLoadUnitsCommand() cli.Command { - return cli.Command{ - Name: "load", - Usage: "Schedule one or more units in the cluster, first submitting them if necessary.", - ArgsUsage: "[--no-block|--block-attempts=N] UNIT...", - Description: `Load one or many units in the cluster into systemd, but do not start. +var cmdLoad = &cobra.Command{ + Use: "load [--no-block|--block-attempts=N] UNIT...", + Short: "Schedule one or more units in the cluster, first submitting them if necessary.", + Long: `Load one or many units in the cluster into systemd, but do not start. Select units to load by glob matching for units in the current working directory or matching the names of previously submitted units. @@ -38,24 +35,25 @@ which means fleetctl will block until it detects that the unit(s) have transitioned to a loaded state. This behaviour can be configured with the respective --block-attempts and --no-block options. Load operations on global units are always non-blocking.`, - Action: makeActionWrapper(runLoadUnits), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "sign", Usage: "DEPRECATED - this option cannot be used"}, - cli.IntFlag{Name: "block-attempts", Value: 0, Usage: "ait until the jobs are loaded, performing up to N attempts before giving up. A value of 0 indicates no limit. Does not apply to global units."}, - cli.BoolFlag{Name: "no-block", Usage: "Do not wait until the jobs have been loaded before exiting. Always the case for global units."}, - cli.BoolFlag{Name: "replace", Usage: "Replace the old scheduled units in the cluster with new versions."}, - }, - } + Run: runWrapper(runLoadUnit), +} + +func init() { + cmdFleet.AddCommand(cmdLoad) + + cmdLoad.Flags().BoolVar(&sharedFlags.Sign, "sign", false, "DEPRECATED - this option cannot be used") + cmdLoad.Flags().IntVar(&sharedFlags.BlockAttempts, "block-attempts", 0, "Wait until the jobs are loaded, performing up to N attempts before giving up. A value of 0 indicates no limit. Does not apply to global units.") + cmdLoad.Flags().BoolVar(&sharedFlags.NoBlock, "no-block", false, "Do not wait until the jobs have been loaded before exiting. Always the case for global units.") + cmdLoad.Flags().BoolVar(&sharedFlags.Replace, "replace", false, "Replace the old scheduled units in the cluster with new versions.") } -func runLoadUnits(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runLoadUnit(cCmd *cobra.Command, args []string) (exit int) { if len(args) == 0 { stderr("No units given") return 0 } - if err := lazyCreateUnits(c); err != nil { + if err := lazyCreateUnits(cCmd, args); err != nil { stderr("Error creating units: %v", err) return 1 } @@ -75,7 +73,11 @@ func runLoadUnits(c *cli.Context, cAPI client.API) (exit int) { } } - exit = tryWaitForUnitStates(loading, "load", job.JobStateLoaded, getBlockAttempts(c), os.Stdout) + exitVal := tryWaitForUnitStates(loading, "load", job.JobStateLoaded, getBlockAttempts(cCmd), os.Stdout) + if exitVal != 0 { + stderr("Error waiting for unit states, exit status: %d", exitVal) + return 1 + } - return + return 0 } diff --git a/fleetctl/ssh.go b/fleetctl/ssh.go index caf2a1c20..7835209cd 100644 --- a/fleetctl/ssh.go +++ b/fleetctl/ssh.go @@ -24,53 +24,60 @@ import ( "strings" "syscall" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" + // "github.com/coreos/fleet/client" "github.com/coreos/fleet/machine" "github.com/coreos/fleet/pkg" "github.com/coreos/fleet/ssh" ) -func NewSSHCommend() cli.Command { - return cli.Command{ - Name: "ssh", - Usage: "Open interactive shell on a machine in the cluster", - ArgsUsage: "[-A|--forward-agent] [--ssh-port=N] [--machine|--unit] {MACHINE|UNIT}", - Description: `Open an interactive shell on a specific machine in the cluster or on the machine where the specified unit is located. +var ( + flagMachine string + flagUnit string + flagSSHAgentForwarding bool +) + +var cmdSSH = &cobra.Command{ + Use: "ssh [-A|--forward-agent] [--ssh-port=N] [--machine|--unit] {MACHINE|UNIT}", + Short: "Open interactive shell on a machine in the cluster", + Long: `Open an interactive shell on a specific machine in the cluster or on the machine +where the specified unit is located. fleetctl tries to detect whether your first argument is a machine or a unit. To skip this check use the --machine or --unit flags. Open a shell on a machine: - fleetctl ssh 2444264c-eac2-4eff-a490-32d5e5e4af24 +fleetctl ssh 2444264c-eac2-4eff-a490-32d5e5e4af24 Open a shell from your laptop, to the machine running a specific unit, using a cluster member as a bastion host: - fleetctl --tunnel 10.10.10.10 ssh foo.service +fleetctl --tunnel 10.10.10.10 ssh foo.service Open a shell on a machine and forward the authentication agent connection: - fleetctl ssh --forward-agent 2444264c-eac2-4eff-a490-32d5e5e4af24 +fleetctl ssh --forward-agent 2444264c-eac2-4eff-a490-32d5e5e4af24 Tip: Create an alias for --tunnel. - - Add "alias fleetctl=fleetctl --tunnel 10.10.10.10" to your bash profile. - - Now you can run all fleet commands locally. +- Add "alias fleetctl=fleetctl --tunnel 10.10.10.10" to your bash profile. +- Now you can run all fleet commands locally. This command does not work with global units.`, - Action: makeActionWrapper(runSSH), - Flags: []cli.Flag{ - cli.StringFlag{Name: "machine", Value: "", Usage: "Open SSH connection to a specific machine."}, - cli.StringFlag{Name: "unit", Value: "", Usage: "Open SSH connection to machine running provided unit."}, - cli.BoolFlag{Name: "forward-agent, A", Usage: "forward local ssh-agent to target machine."}, - cli.IntFlag{Name: "ssh-port", Value: 22, Usage: "Connect to remote hosts over SSH using this TCP port."}, - }, - } + Run: runWrapper(runSSH), } -func runSSH(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() - if c.String("unit") != "" && c.String("machine") != "" { +func init() { + cmdFleet.AddCommand(cmdSSH) + + cmdSSH.Flags().StringVar(&flagMachine, "machine", "", "Open SSH connection to a specific machine.") + cmdSSH.Flags().StringVar(&flagUnit, "unit", "", "Open SSH connection to machine running provided unit.") + cmdSSH.Flags().BoolVar(&flagSSHAgentForwarding, "forward-agent", false, "Forward local ssh-agent to target machine.") + cmdSSH.Flags().BoolVar(&flagSSHAgentForwarding, "A", false, "Shorthand for --forward-agent") + cmdSSH.Flags().IntVar(&sharedFlags.SSHPort, "ssh-port", 22, "Connect to remote hosts over SSH using this TCP port.") +} + +func runSSH(cCmd *cobra.Command, args []string) (exit int) { + if flagUnit != "" && flagMachine != "" { stderr("Both machine and unit flags provided, please specify only one.") return 1 } @@ -79,10 +86,10 @@ func runSSH(c *cli.Context, cAPI client.API) (exit int) { var addr string switch { - case c.String("machine") != "": - addr, _, err = findAddressInMachineList(c.String("machine")) - case c.String("unit") != "": - addr, _, err = findAddressInRunningUnits(c.String("unit")) + case flagMachine != "": + addr, _, err = findAddressInMachineList(flagMachine) + case flagUnit != "": + addr, _, err = findAddressInRunningUnits(flagUnit) default: addr, err = globalMachineLookup(args) // trim machine/unit name from args @@ -101,16 +108,16 @@ func runSSH(c *cli.Context, cAPI client.API) (exit int) { return 1 } - addr = findSSHPort(addr, c) + addr = findSSHPort(cCmd, addr) args = pkg.TrimToDashes(args) var sshClient *ssh.SSHForwardingClient - timeout := getSSHTimeoutFlag(c) - if tun := getTunnelFlag(c); tun != "" { - sshClient, err = ssh.NewTunnelledSSHClient(c.GlobalString("ssh-username"), tun, addr, getChecker(c), c.Bool("forward-agent"), timeout) + timeout := getSSHTimeoutFlag(cCmd) + if tun := getTunnelFlag(cCmd); tun != "" { + sshClient, err = ssh.NewTunnelledSSHClient(globalFlags.SSHUserName, tun, addr, getChecker(cCmd), flagSSHAgentForwarding, timeout) } else { - sshClient, err = ssh.NewSSHClient(c.GlobalString("ssh-username"), addr, getChecker(c), c.Bool("forward-agent"), timeout) + sshClient, err = ssh.NewSSHClient(globalFlags.SSHUserName, addr, getChecker(cCmd), flagSSHAgentForwarding, timeout) } if err != nil { stderr("Failed building SSH client: %v", err) @@ -128,16 +135,16 @@ func runSSH(c *cli.Context, cAPI client.API) (exit int) { } else { if err := ssh.Shell(sshClient); err != nil { stderr("Failed opening shell over SSH: %v", err) - return 1 + exit = 1 } } - return } -func findSSHPort(addr string, c *cli.Context) string { - if c.Int("ssh-port") != 22 && !strings.Contains(addr, ":") { - return net.JoinHostPort(addr, strconv.Itoa(c.Int("ssh-port"))) +func findSSHPort(cCmd *cobra.Command, addr string) string { + SSHPort, _ := cCmd.Flags().GetInt("ssh-port") + if SSHPort != 22 && !strings.Contains(addr, ":") { + return net.JoinHostPort(addr, strconv.Itoa(SSHPort)) } else { return addr } @@ -213,7 +220,7 @@ func findAddressInRunningUnits(name string) (string, bool, error) { // runCommand will attempt to run a command on a given machine. It will attempt // to SSH to the machine if it is identified as being remote. -func runCommand(c *cli.Context, machID string, cmd string, args ...string) (retcode int) { +func runCommand(cCmd *cobra.Command, machID string, cmd string, args ...string) (retcode int) { var err error if machine.IsLocalMachineID(machID) { err, retcode = runLocalCommand(cmd, args...) @@ -225,8 +232,8 @@ func runCommand(c *cli.Context, machID string, cmd string, args ...string) (retc if err != nil || ms == nil { stderr("Error getting machine IP: %v", err) } else { - addr := findSSHPort(ms.PublicIP, c) - err, retcode = runRemoteCommand(c, addr, cmd, args...) + addr := findSSHPort(cCmd, ms.PublicIP) + err, retcode = runRemoteCommand(cCmd, addr, cmd, args...) if err != nil { stderr("Error running remote command: %v", err) } @@ -257,13 +264,13 @@ func runLocalCommand(cmd string, args ...string) (error, int) { // runRemoteCommand runs the given command over SSH on the given IP, and returns // any error encountered and the exit status of the command -func runRemoteCommand(c *cli.Context, addr string, cmd string, args ...string) (err error, exit int) { +func runRemoteCommand(cCmd *cobra.Command, addr string, cmd string, args ...string) (err error, exit int) { var sshClient *ssh.SSHForwardingClient - timeout := getSSHTimeoutFlag(c) - if tun := getTunnelFlag(c); tun != "" { - sshClient, err = ssh.NewTunnelledSSHClient(c.GlobalString("ssh-username"), tun, addr, getChecker(c), false, timeout) + timeout := getSSHTimeoutFlag(cCmd) + if tun := getTunnelFlag(cCmd); tun != "" { + sshClient, err = ssh.NewTunnelledSSHClient(globalFlags.SSHUserName, tun, addr, getChecker(cCmd), false, timeout) } else { - sshClient, err = ssh.NewSSHClient(c.GlobalString("ssh-username"), addr, getChecker(c), false, timeout) + sshClient, err = ssh.NewSSHClient(globalFlags.SSHUserName, addr, getChecker(cCmd), false, timeout) } if err != nil { return err, -1 diff --git a/fleetctl/start.go b/fleetctl/start.go index 2ad4ce690..bb40f2d98 100644 --- a/fleetctl/start.go +++ b/fleetctl/start.go @@ -17,18 +17,17 @@ package main import ( "os" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/job" ) -func NewStartCommand() cli.Command { - return cli.Command{ - Name: "start", - Usage: "Instruct systemd to start one or more units in the cluster, first submitting and loading if necessary.", - ArgsUsage: "[--no-block|--block-attempts=N] UNIT...", - Description: `Start one or many units on the cluster. Select units to start by glob matching for units in the current working directory or matching names of previously submitted units. +var cmdStart = &cobra.Command{ + Use: "start [--no-block|--block-attempts=N] UNIT...", + Short: "Instruct systemd to start one or more units in the cluster, first submitting and loading if necessary.", + Long: `Start one or many units on the cluster. Select units to start by glob matching +for units in the current working directory or matching names of previously +submitted units. For units which are not global, start operations are performed synchronously, which means fleetctl will block until it detects that the unit(s) have @@ -37,31 +36,32 @@ respective --block-attempts and --no-block options. Start operations on global units are always non-blocking. Start a single unit: - fleetctl start foo.service +fleetctl start foo.service Start an entire directory of units with glob matching: - fleetctl start myservice/* +fleetctl start myservice/* You may filter suitable hosts based on metadata provided by the machine. Machine metadata is located in the fleet configuration file.`, - Action: makeActionWrapper(runStartUnit), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "sign", Usage: "DEPRECATED - this option cannot be used"}, - cli.IntFlag{Name: "block-attempts", Value: 0, Usage: "Wait until the units are launched, performing up to N attempts before giving up. A value of 0 indicates no limit. Does not apply to global units."}, - cli.BoolFlag{Name: "no-block", Usage: "Do not wait until the units have launched before exiting. Always the case for global units."}, - cli.BoolFlag{Name: "replace", Usage: "Replace the already started units in the cluster with new versions."}, - }, - } + Run: runWrapper(runStartUnit), +} + +func init() { + cmdFleet.AddCommand(cmdStart) + + cmdStart.Flags().BoolVar(&sharedFlags.Sign, "sign", false, "DEPRECATED - this option cannot be used") + cmdStart.Flags().IntVar(&sharedFlags.BlockAttempts, "block-attempts", 0, "Wait until the units are launched, performing up to N attempts before giving up. A value of 0 indicates no limit. Does not apply to global units.") + cmdStart.Flags().BoolVar(&sharedFlags.NoBlock, "no-block", false, "Do not wait until the units have launched before exiting. Always the case for global units.") + cmdStart.Flags().BoolVar(&sharedFlags.Replace, "replace", false, "Replace the already started units in the cluster with new versions.") } -func runStartUnit(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runStartUnit(cCmd *cobra.Command, args []string) (exit int) { if len(args) == 0 { stderr("No units given") return 0 } - if err := lazyCreateUnits(c); err != nil { + if err := lazyCreateUnits(cCmd, args); err != nil { stderr("Error creating units: %v", err) return 1 } @@ -81,6 +81,11 @@ func runStartUnit(c *cli.Context, cAPI client.API) (exit int) { } } - exit = tryWaitForUnitStates(starting, "start", job.JobStateLaunched, getBlockAttempts(c), os.Stdout) - return + exitVal := tryWaitForUnitStates(starting, "start", job.JobStateLaunched, getBlockAttempts(cCmd), os.Stdout) + if exitVal != 0 { + stderr("Error waiting for unit states, exit status: %d", exitVal) + return exitVal + } + + return 0 } diff --git a/fleetctl/status.go b/fleetctl/status.go index ef9cacf7e..5f85399f1 100644 --- a/fleetctl/status.go +++ b/fleetctl/status.go @@ -17,37 +17,35 @@ package main import ( "fmt" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/job" ) -func NewStatusCommand() cli.Command { - return cli.Command{ - Name: "status", - Usage: "Output the status of one or more units in the cluster", - ArgsUsage: "[--ssh-port=N] UNIT...", - Description: `Output the status of one or more units currently running in the cluster. +var cmdStatus = &cobra.Command{ + Use: "status [--ssh-port=N] UNIT...", + Short: "Output the status of one or more units in the cluster", + Long: `Output the status of one or more units currently running in the cluster. Supports glob matching of units in the current working directory or matches previously started units. Show status of a single unit: - fleetctl status foo.service + fleetctl status foo.service Show status of an entire directory with glob matching: fleetctl status myservice/* This command does not work with global units.`, - Action: makeActionWrapper(runStatusUnits), - Flags: []cli.Flag{ - cli.IntFlag{Name: "ssh-port", Value: 22, Usage: "Connect to remote hosts over SSH using this TCP port."}, - }, - } + Run: runWrapper(runStatusUnit), +} + +func init() { + cmdFleet.AddCommand(cmdStatus) + + cmdStatus.Flags().IntVar(&sharedFlags.SSHPort, "ssh-port", 22, "Connect to remote hosts over SSH using this TCP port.") } -func runStatusUnits(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runStatusUnit(cCmd *cobra.Command, args []string) (exit int) { for i, arg := range args { name := unitNameMangle(arg) unit, err := cAPI.Unit(name) @@ -72,7 +70,8 @@ func runStatusUnits(c *cli.Context, cAPI client.API) (exit int) { fmt.Printf("\n") } - if exit = runCommand(c, unit.MachineID, "systemctl", "status", "-l", unit.Name); exit != 0 { + if exitVal := runCommand(cCmd, unit.MachineID, "systemctl", "status", "-l", unit.Name); exitVal != 0 { + exit = exitVal break } } diff --git a/fleetctl/stop.go b/fleetctl/stop.go index 7ba790ead..2b03c51b6 100644 --- a/fleetctl/stop.go +++ b/fleetctl/stop.go @@ -17,19 +17,17 @@ package main import ( "os" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/job" "github.com/coreos/fleet/log" ) -func NewStopUnitCommand() cli.Command { - return cli.Command{ - Name: "stop", - Usage: "Instruct systemd to stop one or more units in the cluster.", - ArgsUsage: "[--no-block|--block-attempts=N] UNIT...", - Description: `Stop one or more units from running in the cluster, but allow them to be started again in the future. +var cmdStop = &cobra.Command{ + Use: "stop [--no-block|--block-attempts=N] UNIT...", + Short: "Instruct systemd to stop one or more units in the cluster.", + Long: `Stop one or more units from running in the cluster, but allow them to be +started again in the future. Instructs systemd on the host machine to stop the unit, deferring to systemd completely for any custom stop directives (i.e. ExecStop option in the unit @@ -42,20 +40,21 @@ respective --block-attempts and --no-block options. Stop operations on global units are always non-blocking. Stop a single unit: - fleetctl stop foo.service +fleetctl stop foo.service Stop an entire directory of units with glob matching, without waiting: - fleetctl --no-block stop myservice/*`, - Action: makeActionWrapper(runStopUnit), - Flags: []cli.Flag{ - cli.IntFlag{Name: "block-attempts", Value: 22, Usage: "Wait until the units are stopped, performing up to N attempts before giving up. A value of 0 indicates no limit. Does not apply to global units."}, - cli.BoolFlag{Name: "no-block", Usage: "Do not wait until the units have stopped before exiting. Always the case for global units."}, - }, - } +fleetctl --no-block stop myservice/*`, + Run: runWrapper(runStopUnit), +} + +func init() { + cmdFleet.AddCommand(cmdStop) + + cmdStop.Flags().IntVar(&sharedFlags.BlockAttempts, "block-attempts", 0, "Wait until the units are stopped, performing up to N attempts before giving up. A value of 0 indicates no limit. Does not apply to global units.") + cmdStop.Flags().BoolVar(&sharedFlags.NoBlock, "no-block", false, "Do not wait until the units have stopped before exiting. Always the case for global units.") } -func runStopUnit(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runStopUnit(cCmd *cobra.Command, args []string) (exit int) { if len(args) == 0 { stderr("No units given") return 0 @@ -88,7 +87,7 @@ func runStopUnit(c *cli.Context, cAPI client.API) (exit int) { } } - exit = tryWaitForUnitStates(stopping, "stop", job.JobStateLoaded, getBlockAttempts(c), os.Stdout) + exit = tryWaitForUnitStates(stopping, "stop", job.JobStateLoaded, getBlockAttempts(cCmd), os.Stdout) if exit == 0 { stderr("Successfully stopped units %v.", stopping) } else { diff --git a/fleetctl/submit.go b/fleetctl/submit.go index fc55aaaf9..b5ef4b874 100644 --- a/fleetctl/submit.go +++ b/fleetctl/submit.go @@ -15,44 +15,41 @@ package main import ( - "github.com/codegangsta/cli" - - "github.com/coreos/fleet/client" + "github.com/spf13/cobra" ) -func NewSubmitUnitCommand() cli.Command { - return cli.Command{ - Name: "submit", - Usage: "Upload one or more units to the cluster without starting them", - ArgsUsage: "UNIT...", - Description: `Upload one or more units to the cluster without starting them. Useful for validating units before they are started. +var cmdSubmit = &cobra.Command{ + Use: "submit UNIT...", + Short: "Upload one or more units to the cluster without starting them", + Long: `Upload one or more units to the cluster without starting them. Useful +for validating units before they are started. This operation is idempotent; if a named unit already exists in the cluster, it will not be resubmitted. Submit a single unit: - fleetctl submit foo.service +fleetctl submit foo.service Submit a directory of units with glob matching: - fleetctl submit myservice/*`, - Action: makeActionWrapper(runSubmitUnits), - Flags: []cli.Flag{ - cli.BoolFlag{Name: "sign", Usage: "DEPRECATED - this option cannot be used"}, - cli.BoolFlag{Name: "replace", Usage: "Replace the old submitted units in the cluster with new versions."}, - }, - } +fleetctl submit myservice/*`, + Run: runWrapper(runSubmitUnit), +} + +func init() { + cmdFleet.AddCommand(cmdSubmit) + + cmdSubmit.Flags().BoolVar(&sharedFlags.Sign, "sign", false, "DEPRECATED - this option cannot be used") + cmdSubmit.Flags().BoolVar(&sharedFlags.Replace, "replace", false, "Replace the old submitted units in the cluster with new versions.") } -func runSubmitUnits(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runSubmitUnit(cCmd *cobra.Command, args []string) (exit int) { if len(args) == 0 { stderr("No units given") return 0 } - if err := lazyCreateUnits(c); err != nil { + if err := lazyCreateUnits(cCmd, args); err != nil { stderr("Error creating units: %v", err) - exit = 1 + return 1 } - - return + return 0 } diff --git a/fleetctl/unload.go b/fleetctl/unload.go index d7a98fdea..238b89993 100644 --- a/fleetctl/unload.go +++ b/fleetctl/unload.go @@ -17,28 +17,26 @@ package main import ( "os" - "github.com/codegangsta/cli" + "github.com/spf13/cobra" - "github.com/coreos/fleet/client" "github.com/coreos/fleet/job" "github.com/coreos/fleet/log" ) -func NewUnloadUnitCommand() cli.Command { - return cli.Command{ - Name: "unload", - Usage: "Unschedule one or more units in the cluster.", - ArgsUsage: "UNIT...", - Action: makeActionWrapper(runUnloadUnit), - Flags: []cli.Flag{ - cli.IntFlag{Name: "block-attempts", Value: 0, Usage: "Wait until the units are inactive, performing up to N attempts before giving up. A value of 0 indicates no limit."}, - cli.BoolFlag{Name: "no-block", Usage: "Do not wait until the units have become inactive before exiting."}, - }, - } +var cmdUnload = &cobra.Command{ + Use: "unload UNIT...", + Short: "Unschedule one or more units in the cluster.", + Run: runWrapper(runUnloadUnit), +} + +func init() { + cmdFleet.AddCommand(cmdUnload) + + cmdUnload.Flags().IntVar(&sharedFlags.BlockAttempts, "block-attempts", 0, "Wait until the units are inactive, performing up to N attempts before giving up. A value of 0 indicates no limit.") + cmdUnload.Flags().BoolVar(&sharedFlags.NoBlock, "no-block", false, "Do not wait until the units have become inactive before exiting.") } -func runUnloadUnit(c *cli.Context, cAPI client.API) (exit int) { - args := c.Args() +func runUnloadUnit(cCmd *cobra.Command, args []string) (exit int) { if len(args) == 0 { stderr("No units given") return 0 @@ -68,7 +66,7 @@ func runUnloadUnit(c *cli.Context, cAPI client.API) (exit int) { } } - exit = tryWaitForUnitStates(wait, "unload", job.JobStateInactive, getBlockAttempts(c), os.Stdout) + exit = tryWaitForUnitStates(wait, "unload", job.JobStateInactive, getBlockAttempts(cCmd), os.Stdout) if exit == 0 { stderr("Successfully unloaded units %v.", wait) } else { diff --git a/fleetctl/verify.go b/fleetctl/verify.go index 2081982b2..b54b607d8 100644 --- a/fleetctl/verify.go +++ b/fleetctl/verify.go @@ -15,21 +15,20 @@ package main import ( - "github.com/codegangsta/cli" - - "github.com/coreos/fleet/client" + "github.com/spf13/cobra" ) -func NewVerifyCommand() cli.Command { - return cli.Command{ - Name: "verify", - Usage: "DEPRECATED - No longer works", - ArgsUsage: "UNIT", - Action: makeActionWrapper(runVerifyUnit), - } +var cmdVerifyUnit = &cobra.Command{ + Use: "verify UNIT", + Deprecated: "DEPRECATED - No longer works", + Run: runWrapper(runVerifyUnit), +} + +func init() { + cmdFleet.AddCommand(cmdVerifyUnit) } -func runVerifyUnit(c *cli.Context, cAPI client.API) (exit int) { +func runVerifyUnit(cCmd *cobra.Command, args []string) (exit int) { stderr("WARNING: The signed/verified units feature is DEPRECATED and cannot be used.") return 2 } diff --git a/fleetctl/version.go b/fleetctl/version.go new file mode 100644 index 000000000..2b5eb3894 --- /dev/null +++ b/fleetctl/version.go @@ -0,0 +1,37 @@ +// Copyright 2016 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE+2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/spf13/cobra" + + "github.com/coreos/fleet/version" +) + +var cmdVersion = &cobra.Command{ + Use: "version", + Short: "Print the version and exit", + Long: "Print the version and exit", + Run: runWrapper(runVersion), +} + +func init() { + cmdFleet.AddCommand(cmdVersion) +} + +func runVersion(cCmd *cobra.Command, args []string) (exit int) { + stdout("fleetctl version %s", version.Version) + return 0 +}