func newLogger(name string, debug bool) logging.Logger { log := logging.NewLogger(name) logHandler := logging.NewWriterHandler(os.Stderr) logHandler.Colorize = true log.SetHandler(logHandler) if debug { log.SetLevel(logging.DEBUG) logHandler.SetLevel(logging.DEBUG) } return log }
func TestMain(m *testing.M) { var err error vg, err = NewVagrant(vagrantName) if err != nil { log.Fatalln(err) } vg.Log = logging.NewLogger("vagrantutil_test") vg.Log.SetLevel(logging.DEBUG) h := logging.NewWriterHandler(os.Stderr) h.SetLevel(logging.DEBUG) vg.Log.SetHandler(h) ret := m.Run() os.RemoveAll(vagrantName) os.Exit(ret) }
func init() { discardLogger = logging.NewLogger("test") discardLogger.SetHandler(logging.NewWriterHandler(ioutil.Discard)) }
// UpdateCommand updates this binary if there's an update available. func UpdateCommand(c *cli.Context, log logging.Logger, _ string) int { if len(c.Args()) != 0 { cli.ShowCommandHelp(c, "update") return 1 } var ( forceUpdate = c.Bool("force") klientVersion = c.Int("klient-version") klientChannel = c.String("klient-channel") kdVersion = c.Int("kd-version") kdChannel = c.String("kd-channel") continueUpdate = c.Bool("continue") ) if kdChannel == "" { kdChannel = config.Environment } if klientChannel == "" { klientChannel = config.Environment } // Create and open the log file, to be safe in case it's missing. f, err := createLogFile(LogFilePath) if err != nil { fmt.Println(`Error: Unable to open log files.`) } else { log.SetHandler(logging.NewWriterHandler(f)) log.Info("Update created log file at %q", LogFilePath) } if !shouldTryUpdate(kdVersion, klientVersion, forceUpdate) { yesUpdate, err := checkUpdate() if err != nil { log.Error("Error checking if update is available. err:%s", err) fmt.Println(FailedCheckingUpdateAvailable) return 1 } if !yesUpdate { fmt.Println("No update available.") return 0 } else { fmt.Println("An update is available.") } } if kdVersion == 0 { var err error kdVersion, err = latestVersion(config.Konfig.Endpoints.KDLatest.Public.String()) if err != nil { log.Error("Error fetching klientctl update version. err: %s", err) fmt.Println(FailedCheckingUpdateAvailable) return 1 } } if klientVersion == 0 { var err error klientVersion, err = latestVersion(config.Konfig.Endpoints.KlientLatest.Public.String()) if err != nil { log.Error("Error fetching klient update version. err: %s", err) fmt.Println(FailedCheckingUpdateAvailable) return 1 } } klientPath := filepath.Join(KlientDirectory, "klient") klientctlPath := filepath.Join(KlientctlDirectory, "kd") klientUrl := config.S3Klient(klientVersion, klientChannel) klientctlUrl := config.S3Klientctl(kdVersion, kdChannel) // If --continue is not passed, download kd and then call the new kd binary with // `kd update --continue`, so that the new code handles updates to klient.sh, // service, and any migration code needed. if !continueUpdate { // Only show this message once. fmt.Println("Updating...") if err := downloadRemoteToLocal(klientctlUrl, klientctlPath); err != nil { log.Error("Error downloading klientctl. err:%s", err) fmt.Println(FailedDownloadUpdate) return 1 } flags := flagsFromContext(c) // Very important to pass --continue for the subprocess. // --force also helps ensure it updates, since the subprocess is technically // the latest KD version. flags = append([]string{"update", "--continue=true", "--force=true"}, flags...) log.Info("%s", flags) cmd := exec.Command(klientctlPath, flags...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Error("error during --continue update. err: %s", err) return 1 } return 0 } klientSh := klientSh{ User: konfig.CurrentUser.Username, KlientBinPath: filepath.Join(KlientDirectory, "klient"), } // ensure the klient home dir is writeable by user if klientSh.User != "" { ensureWriteable(KlientctlDirectory, klientSh.User) } opts := &ServiceOptions{ Username: klientSh.User, } s, err := newService(opts) if err != nil { log.Error("Error creating Service. err:%s", err) fmt.Println(GenericInternalNewCodeError) return 1 } fmt.Printf("Stopping %s...\n", config.KlientName) // stop klient before we update it if err := s.Stop(); err != nil { log.Error("Error stopping Service. err:%s", err) fmt.Println(FailedStopKlient) return 1 } if err := downloadRemoteToLocal(klientUrl, klientPath); err != nil { log.Error("Error downloading klient. err:%s", err) fmt.Println(FailedDownloadUpdate) return 1 } klientScript := filepath.Join(KlientDirectory, "klient.sh") if err := klientSh.Create(klientScript); err != nil { log.Error("Error writing klient.sh file. err:%s", err) fmt.Println(FailedInstallingKlient) return 1 } // try to migrate from old managed klient to new kd-installed klient switch runtime.GOOS { case "darwin": oldS, err := service.New(&serviceProgram{}, &service.Config{ Name: "com.koding.klient", Executable: klientScript, }) if err != nil { break } oldS.Stop() oldS.Uninstall() } // try to uninstall first, otherwise Install may fail if // klient.plist or klient init script already exist s.Uninstall() // Install the klient binary as a OS service if err = s.Install(); err != nil { log.Error("Error installing Service. err:%s", err) fmt.Println(GenericInternalNewCodeError) return 1 } // start klient now that it's done updating if err := s.Start(); err != nil { log.Error("Error starting Service. err:%s", err) fmt.Println(FailedStartKlient) return 1 } // Best-effort attempts at fixinig permissions and ownership, ignore any errors. _ = uploader.FixPerms() _ = configstore.FixOwner() fmt.Printf("Successfully updated to latest version of %s.\n", config.Name) return 0 }
// The implementation of InstallCommandFactory, with an error return. This // allows us to track the error metrics. func InstallCommandFactory(c *cli.Context, log logging.Logger, _ string) (exit int, err error) { if len(c.Args()) != 1 { cli.ShowCommandHelp(c, "install") return 1, errors.New("incorrect cli usage: no args") } // Now that we created the logfile, set our logger handler to use that newly created // file, so that we can log errors during installation. f, err := createLogFile(LogFilePath) if err != nil { fmt.Println(`Error: Unable to open log files.`) } else { log.SetHandler(logging.NewWriterHandler(f)) log.Info("Installation created log file at %q", LogFilePath) } // Track all failed installations. defer func() { if err != nil { log.Error(err.Error()) metrics.TrackInstallFailed(err.Error(), config.VersionNum()) } }() authToken := c.Args().Get(0) // We need to check if the authToken is somehow empty, because klient // will default to user/pass if there is no auth token (despite setting // the token flag) if strings.TrimSpace(authToken) == "" { cli.ShowCommandHelp(c, "install") return 1, errors.New("incorrect cli usage: missing token") } // Create the installation dir, if needed. if err := os.MkdirAll(KlientDirectory, 0755); err != nil { log.Error( "Error creating klient binary directory(s). path:%s, err:%s", KlientDirectory, err, ) fmt.Println(FailedInstallingKlient) return 1, fmt.Errorf("failed creating klient binary: %s", err) } klientBinPath := filepath.Join(KlientDirectory, "klient") // TODO: Accept `kd install --user foo` flag to replace the // environ checking. klientSh := klientSh{ User: konfig.CurrentUser.Username, KlientBinPath: klientBinPath, } if err := klientSh.Create(filepath.Join(KlientDirectory, "klient.sh")); err != nil { err = fmt.Errorf("error writing klient.sh file: %s", err) fmt.Println(FailedInstallingKlient) return 1, err } fmt.Println("Downloading...") version, err := latestVersion(config.Konfig.Endpoints.KlientLatest.Public.String()) if err != nil { fmt.Printf(FailedDownloadingKlient) return 1, fmt.Errorf("error getting latest klient version: %s", err) } if err := downloadRemoteToLocal(config.S3Klient(version, config.Environment), klientBinPath); err != nil { fmt.Printf(FailedDownloadingKlient) return 1, fmt.Errorf("error downloading klient binary: %s", err) } fmt.Printf("Created %s\n", klientBinPath) fmt.Printf(`Authenticating you to the %s `, config.KlientName) cmd := exec.Command(klientBinPath, "-register", "-token", authToken, "--kontrol-url", strings.TrimSpace(c.String("kontrol")), ) var errBuf bytes.Buffer // Note that we are *only* printing to Stdout. This is done because // Klient logs error messages to Stderr, and we want to control the UX for // that interaction. // // TODO: Logg Klient's Stderr message on error, if any. cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin cmd.Stderr = &errBuf if err := cmd.Run(); err != nil { err = fmt.Errorf("error registering klient: %s, klient stderr: %s", err, errBuf.String()) fmt.Println(FailedRegisteringKlient) return 1, err } // Best-effort attempts at fixinig permissions and ownership, ignore any errors. _ = configstore.FixOwner() opts := &ServiceOptions{ Username: klientSh.User, } // Create our interface to the OS specific service s, err := newService(opts) if err != nil { fmt.Println(GenericInternalNewCodeError) return 1, fmt.Errorf("error creating Service: %s", err) } // try to uninstall first, otherwise Install may fail if // klient.plist or klient init script already exist s.Uninstall() // Install the klient binary as a OS service if err := s.Install(); err != nil { fmt.Println(GenericInternalNewCodeError) return 1, fmt.Errorf("error installing Service: %s", err) } // Tell the service to start. Normally it starts automatically, but // if the user told the service to stop (previously), it may not // start automatically. // // Note that the service may error if it is already running, so // we're ignoring any starting errors here. We will verify the // connection below, anyway. if err := s.Start(); err != nil { fmt.Println(FailedStartKlient) return 1, fmt.Errorf("error starting klient service: %s", err) } fmt.Println("Verifying installation...") err = WaitUntilStarted(config.Konfig.Endpoints.Klient.Private.String(), CommandAttempts, CommandWaitTime) // After X times, if err != nil we failed to connect to klient. // Inform the user. if err != nil { fmt.Println(FailedInstallingKlient) return 1, fmt.Errorf("error verifying the installation of klient: %s", err) } // Best-effort attempts at fixinig permissions and ownership, ignore any errors. _ = uploader.FixPerms() // track metrics metrics.TrackInstall(config.VersionNum()) fmt.Printf("\n\nSuccessfully installed and started the %s!\n", config.KlientName) return 0, nil }
func init() { DiscardLogger = logging.NewLogger("DiscardLogger") DiscardLogger.SetHandler(logging.NewWriterHandler(ioutil.Discard)) }
func run(args []string) { // For forward-compatibility with go1.5+, where GOMAXPROCS is // always set to a number of available cores. runtime.GOMAXPROCS(runtime.NumCPU()) debug = debug || config.Konfig.Debug // The writer used for the logging output. Either a file, or /dev/null var logWriter io.Writer f, err := os.OpenFile(LogFilePath, os.O_WRONLY|os.O_APPEND, 0666) if err != nil { // Rather than exit `kd` because we are unable to log, we simple disable logging // by writing to /dev/null. This also measn that even if we can't load the log // file, the log instance is valid and doesn't have to be checked for being // nil before every usage. logWriter = ioutil.Discard } else { logWriter = f ctlcli.CloseOnExit(f) } // Setting the handler to debug, because various methods allow a // debug option, and this saves us from having to set the handler level every time. // This only sets handler, not the actual loglevel. handler := logging.NewWriterHandler(logWriter) handler.SetLevel(logging.DEBUG) // Create our logger. // // TODO: Single commit temporary solution, need to remove the above logger // in favor of this. log = logging.NewLogger("kd") log.SetHandler(handler) log.Info("kd binary called with: %s", os.Args) if debug { log.SetLevel(logging.DEBUG) } // Check if the command the user is giving requires sudo. if err := AdminRequired(os.Args, sudoRequiredFor, util.NewPermissions()); err != nil { // In the event of an error, simply print the error to the user // and exit. fmt.Println("Error: this command requires sudo.") ctlcli.Close() os.Exit(10) } kloud.DefaultLog = log testKloudHook(kloud.DefaultClient) defer ctlcli.Close() // TODO(leeola): deprecate this default, instead passing it as a dependency // to the users of it. // // init the defaultHealthChecker with the log. defaultHealthChecker = NewDefaultHealthChecker(log) app := cli.NewApp() app.Name = config.Name app.Version = getReadableVersion(config.Version) app.EnableBashCompletion = true app.Commands = []cli.Command{ { Name: "list", ShortName: "ls", Usage: "List running machines for user.", Action: ctlcli.ExitAction(CheckUpdateFirst(ListCommand, log, "list")), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format", }, cli.BoolFlag{ Name: "all", Usage: "Include machines that have been offline for more than 24h.", }, }, Subcommands: []cli.Command{ { Name: "mounts", Usage: "List the mounted machines.", Action: ctlcli.ExitAction(CheckUpdateFirst(MountsCommand, log, "mounts")), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format", }, }, }, }, }, { Name: "version", Usage: "Display version information.", HideHelp: true, Description: cmdDescriptions["version"], Action: ctlcli.ExitAction(VersionCommand, log, "version"), }, { Name: "mount", ShortName: "m", Usage: "Mount a remote folder to a local folder.", Description: cmdDescriptions["mount"], Flags: []cli.Flag{ cli.StringFlag{ Name: "remotepath, r", Usage: "Full path of remote folder in machine to mount.", }, cli.BoolFlag{ Name: "oneway-sync, s", Usage: "Copy remote folder to local and sync on interval. (fastest runtime).", }, cli.IntFlag{ Name: "oneway-interval", Usage: "Sets how frequently local folder will sync with remote, in seconds. ", Value: 2, }, cli.BoolFlag{ Name: "fuse, f", Usage: "Mount the remote folder via Fuse.", }, cli.BoolFlag{ Name: "noprefetch-meta, p", Usage: "For fuse: Retrieve only top level folder/files. Rest is fetched on request (fastest to mount).", }, cli.BoolFlag{ Name: "prefetch-all, a", Usage: "For fuse: Prefetch all contents of the remote directory up front.", }, cli.IntFlag{ Name: "prefetch-interval", Usage: "For fuse: Sets how frequently remote folder will sync with local, in seconds.", }, cli.BoolFlag{ Name: "nowatch, w", Usage: "For fuse: Disable watching for changes on remote machine.", }, cli.BoolFlag{ Name: "noignore, i", Usage: "For fuse: Retrieve all files and folders, including ignored folders like .git & .svn.", }, cli.BoolFlag{ Name: "trace, t", Usage: "Turn on trace logs.", }, }, Action: ctlcli.FactoryAction(MountCommandFactory, log, "mount"), BashComplete: ctlcli.FactoryCompletion( MountCommandFactory, log, "mount", ), }, { Name: "unmount", ShortName: "u", Usage: "Unmount previously mounted machine.", Description: cmdDescriptions["unmount"], Action: ctlcli.FactoryAction(UnmountCommandFactory, log, "unmount"), }, { Name: "remount", ShortName: "r", Usage: "Remount previously mounted machine using same settings.", Description: cmdDescriptions["remount"], Action: ctlcli.ExitAction(RemountCommandFactory, log, "remount"), }, { Name: "ssh", ShortName: "s", Usage: "SSH into the machine.", Description: cmdDescriptions["ssh"], Flags: []cli.Flag{ cli.BoolFlag{ Name: "debug", }, cli.StringFlag{ Name: "username", Usage: "The username to ssh into on the remote machine.", }, }, Action: ctlcli.ExitAction(CheckUpdateFirst(SSHCommandFactory, log, "ssh")), }, { Name: "run", Usage: "Run command on remote or local machine.", Description: cmdDescriptions["run"], Action: ctlcli.ExitAction(RunCommandFactory, log, "run"), SkipFlagParsing: true, }, { Name: "repair", Usage: "Repair the given mount", Action: ctlcli.FactoryAction(RepairCommandFactory, log, "repair"), }, { Name: "status", Usage: fmt.Sprintf("Check status of the %s.", config.KlientName), Description: cmdDescriptions["status"], Action: ctlcli.ExitAction(StatusCommand, log, "status"), }, { Name: "update", Usage: fmt.Sprintf("Update %s to latest version.", config.KlientName), Description: cmdDescriptions["update"], Action: ctlcli.ExitAction(UpdateCommand, log, "update"), Flags: []cli.Flag{ cli.IntFlag{ Name: "kd-version", Usage: "Version of KD (klientctl) to update to.", }, cli.StringFlag{ Name: "kd-channel", Usage: "Channel (production|development) to download update from.", }, cli.IntFlag{ Name: "klient-version", Usage: "Version of klient to update to.", }, cli.StringFlag{ Name: "klient-channel", Usage: "Channel (production|development) to download update from.", }, cli.BoolFlag{ Name: "force", Usage: "Updates kd & klient to latest available version.", }, cli.BoolFlag{ Name: "continue", Usage: "Internal use only.", Hidden: true, }, }, }, { Name: "restart", Usage: fmt.Sprintf("Restart the %s.", config.KlientName), Description: cmdDescriptions["restart"], Action: ctlcli.ExitAction(RestartCommand, log, "restart"), }, { Name: "start", Usage: fmt.Sprintf("Start the %s.", config.KlientName), Description: cmdDescriptions["start"], Action: ctlcli.ExitAction(StartCommand, log, "start"), }, { Name: "stop", Usage: fmt.Sprintf("Stop the %s.", config.KlientName), Description: cmdDescriptions["stop"], Action: ctlcli.ExitAction(StopCommand, log, "stop"), }, { Name: "uninstall", Usage: fmt.Sprintf("Uninstall the %s.", config.KlientName), Description: cmdDescriptions["uninstall"], Action: ExitWithMessage(UninstallCommand, log, "uninstall"), }, { Name: "install", Usage: fmt.Sprintf("Install the %s.", config.KlientName), Description: cmdDescriptions["install"], Flags: []cli.Flag{ cli.StringFlag{ Name: "kontrol, k", Usage: "Specify an alternate Kontrol", }, }, Action: ctlcli.ExitErrAction(InstallCommandFactory, log, "install"), }, { Name: "metrics", Usage: fmt.Sprintf("Internal use only."), HideHelp: true, Action: ctlcli.ExitAction(MetricsCommandFactory, log, "metrics"), }, { Name: "autocompletion", Usage: "Enable autocompletion support for bash and fish shells", Description: cmdDescriptions["autocompletion"], Flags: []cli.Flag{ cli.StringFlag{ Name: "fish-dir", Usage: "The name of directory to add fish autocompletion script.", }, cli.BoolFlag{ Name: "no-bashrc", Usage: "Disable appending autocompletion source command to your bash config file.", }, cli.StringFlag{ Name: "bash-dir", Usage: "The name of directory to add bash autocompletion script.", }, }, Action: ctlcli.FactoryAction( AutocompleteCommandFactory, log, "autocompletion", ), BashComplete: ctlcli.FactoryCompletion( AutocompleteCommandFactory, log, "autocompletion", ), }, { Name: "cp", Usage: fmt.Sprintf( "Copy a file from one one machine to another", ), Description: cmdDescriptions["cp"], Flags: []cli.Flag{ cli.BoolFlag{ Name: "debug", }, }, Action: ctlcli.FactoryAction( CpCommandFactory, log, "cp", ), BashComplete: ctlcli.FactoryCompletion( CpCommandFactory, log, "cp", ), }, { Name: "log", Usage: "Display logs.", Flags: []cli.Flag{ cli.BoolFlag{Name: "debug", Hidden: true}, cli.BoolFlag{Name: "no-kd-log"}, cli.BoolFlag{Name: "no-klient-log"}, cli.StringFlag{Name: "kd-log-file"}, cli.StringFlag{Name: "klient-log-file"}, cli.IntFlag{Name: "lines, n"}, }, Action: ctlcli.FactoryAction(LogCommandFactory, log, "log"), }, { Name: "open", Usage: fmt.Sprintf( "Open the given file(s) on the Koding UI", ), Description: cmdDescriptions["open"], Flags: []cli.Flag{ cli.BoolFlag{Name: "debug"}, }, Action: ctlcli.FactoryAction(OpenCommandFactory, log, "log"), }, } if experimental { app.Commands = append(app.Commands, cli.Command{ Name: "auth", Usage: "User authorization.", Subcommands: []cli.Command{ { Name: "login", Usage: "Log in to your kd.io or koding.com account.", Action: ctlcli.ExitErrAction(AuthLogin, log, "login"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, cli.StringFlag{ Name: "team, t", Usage: "Specify a Koding team to log in. Leaving empty logs in to kd.io by default.", }, cli.StringFlag{ Name: "baseurl", Usage: "Specify a Koding endpoint to log in.", Value: config.Konfig.Endpoints.Koding.Public.String(), }, }, }, // command: kd auth register auth.NewRegisterSubCommand(log), }, }, cli.Command{ Name: "config", Usage: "Manage tool configuration.", Subcommands: []cli.Command{{ Name: "show", Usage: "Show configuration.", Action: ctlcli.ExitErrAction(ConfigShow, log, "show"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "defaults", Usage: "Show also default configuration", }, cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, }, }, { Name: "list", ShortName: "ls", Usage: "List all available configurations.", Action: ctlcli.ExitErrAction(ConfigList, log, "list"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, }, }, { Name: "use", Usage: "Change active configuration.", Action: ctlcli.ExitErrAction(ConfigUse, log, "use"), }, { Name: "set", Usage: "Set a value for the given key, overwriting default one.", Action: ctlcli.ExitErrAction(ConfigSet, log, "set"), }, { Name: "unset", Usage: "Unset the given key, restoring the defaut value.", Action: ctlcli.ExitErrAction(ConfigUnset, log, "set"), }, { Name: "reset", Usage: "Resets configuration to the default value fetched from Koding.", Action: ctlcli.ExitErrAction(ConfigReset, log, "reset"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "force", Usage: "Force retrieving configuration from Koding.", }, }, }}, }, cli.Command{ Name: "credential", ShortName: "c", Usage: "Manage stack credentials.", Subcommands: []cli.Command{{ Name: "list", ShortName: "ls", Usage: "List imported stack credentials.", Action: ctlcli.ExitErrAction(CredentialList, log, "list"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, cli.StringFlag{ Name: "provider, p", Usage: "Specify credential provider.", }, cli.StringFlag{ Name: "team, t", Usage: "Specify team which the credential belongs to.", }, }, }, { Name: "create", Usage: "Create new stack credential.", Action: ctlcli.ExitErrAction(CredentialCreate, log, "create"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, cli.StringFlag{ Name: "provider, p", Usage: "Specify credential provider.", }, cli.StringFlag{ Name: "file, f", Value: "", Usage: "Read credential from a file.", }, cli.StringFlag{ Name: "team, t", Usage: "Specify team which the credential belongs to.", }, cli.StringFlag{ Name: "title", Usage: "Specify credential title.", }, }, }, { Name: "use", Usage: "Change default credential per provider.", Action: ctlcli.ExitErrAction(CredentialUse, log, "use"), }, { Name: "describe", Usage: "Describe credential documents.", Action: ctlcli.ExitErrAction(CredentialDescribe, log, "describe"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, cli.StringFlag{ Name: "provider, p", Usage: "Specify credential provider.", }, }, }}, }, cli.Command{ Name: "machine", Usage: "Manage remote machines.", Subcommands: []cli.Command{{ Name: "list", ShortName: "ls", Usage: "List available machines.", Action: ctlcli.ExitErrAction(MachineListCommand, log, "list"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, }, }, { Name: "ssh", ShortName: "s", Usage: "SSH into provided remote machine.", Action: ctlcli.ExitErrAction(MachineSSHCommand, log, "ssh"), Flags: []cli.Flag{ cli.StringFlag{ Name: "username", Usage: "Remote machine username.", }, }, }}, }, cli.Command{ Name: "stack", Usage: "Manage stacks.", Subcommands: []cli.Command{{ Name: "create", Usage: "Create new stack.", Action: ctlcli.ExitErrAction(StackCreate, log, "create"), Flags: []cli.Flag{ cli.StringFlag{ Name: "provider, p", Usage: "Specify stack provider.", }, cli.StringSliceFlag{ Name: "credential, c", Usage: "Specify stack credentials.", }, cli.StringFlag{ Name: "team, t", Usage: "Specify team which the stack belongs to.", }, cli.StringFlag{ Name: "file, f", Value: "kd.yml", Usage: "Read stack template from a file.", }, cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, }, }}, }, cli.Command{ Name: "team", Usage: "List available teams and set team context.", Subcommands: []cli.Command{{ Name: "show", Usage: "Shows your currently used team.", Action: ctlcli.ExitErrAction(TeamShow, log, "show"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, }, }, { Name: "list", Usage: "Lists user's teams.", Action: ctlcli.ExitErrAction(TeamList, log, "list"), Flags: []cli.Flag{ cli.StringFlag{ Name: "slug", Value: "", Usage: "Limits the output to the specified team slug", }, cli.BoolFlag{ Name: "json", Usage: "Output in JSON format.", }, }, }, }, }, ) } app.Run(args) }