diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index 49c88f33278..f49abdb44b6 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -155,7 +155,7 @@ type serverCollection struct { func (s *serverCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Hostname", "Name", "Address", "Labels"}) + printHeader(t, []string{"Hostname", "UUID", "Address", "Labels"}) if len(s.servers) == 0 { _, err := io.WriteString(w, t.String()) return trace.Wrap(err) diff --git a/tool/tctl/common/node_command.go b/tool/tctl/common/node_command.go index 3323909f80c..bcdfb5c83b7 100644 --- a/tool/tctl/common/node_command.go +++ b/tool/tctl/common/node_command.go @@ -23,8 +23,10 @@ import ( "strings" "time" + "github.com/gravitational/kingpin" "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/auth" + "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" "github.com/gravitational/trace" ) @@ -44,6 +46,42 @@ type NodeCommand struct { ttl time.Duration // namespace is node namespace namespace string + + // CLI subcommands (clauses) + nodeAdd *kingpin.CmdClause + nodeList *kingpin.CmdClause +} + +// Initialize allows NodeCommand to plug itself into the CLI parser +func (c *NodeCommand) Initialize(app *kingpin.Application, config *service.Config) { + c.config = config + + // add node command + nodes := app.Command("nodes", "Issue invites for other nodes to join the cluster") + c.nodeAdd = nodes.Command("add", "Generate a node invitation token") + c.nodeAdd.Flag("roles", "Comma-separated list of roles for the new node to assume [node]").Default("node").StringVar(&c.roles) + c.nodeAdd.Flag("ttl", "Time to live for a generated token").Default(defaults.ProvisioningTokenTTL.String()).DurationVar(&c.ttl) + c.nodeAdd.Flag("count", "add count tokens and output JSON with the list").Hidden().Default("1").IntVar(&c.count) + c.nodeAdd.Flag("format", "output format, 'text' or 'json'").Hidden().Default("text").StringVar(&c.format) + c.nodeAdd.Alias(AddNodeHelp) + + c.nodeList = nodes.Command("ls", "List all active SSH nodes within the cluster") + c.nodeList.Flag("namespace", "Namespace of the nodes").Default(defaults.Namespace).StringVar(&c.namespace) + c.nodeList.Alias(ListNodesHelp) +} + +// TryRun takes the CLI command as an argument (like "nodes ls") and executes it. +func (c *NodeCommand) TryRun(cmd string, client *auth.TunClient) (match bool, err error) { + switch cmd { + case c.nodeAdd.FullCommand(): + err = c.Invite(client) + case c.nodeList.FullCommand(): + err = c.ListActive(client) + + default: + return false, nil + } + return true, trace.Wrap(err) } // Invite generates a token which can be used to add another SSH node @@ -83,7 +121,6 @@ func (u *NodeCommand) Invite(client *auth.TunClient) error { } fmt.Printf(" - This invitation token will expire in %d minutes\n", int(u.ttl.Minutes())) fmt.Printf(" - %v must be reachable from the new node, see --advertise-ip server flag\n", authServers[0].GetAddr()) - fmt.Printf(` - For tokens of type "trustedcluster", tctl needs to be used to create a TrustedCluster resource. See the Admin Guide for more details.`) } else { out, err := json.Marshal(tokens) if err != nil { diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index de84c2c4b16..1c34cd49b53 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -31,16 +31,98 @@ import ( "github.com/Sirupsen/logrus" "github.com/buger/goterm" + "github.com/gravitational/kingpin" "github.com/gravitational/trace" "golang.org/x/crypto/ssh" ) -type CLIConfig struct { +// GlobalCLIFlags keeps the CLI flags that apply to all tctl commands +type GlobalCLIFlags struct { Debug bool ConfigFile string ConfigString string } +// CLICommand interface must be implemented by every CLI command +// +// This allows OSS and Enterprise Teleport editions to plug their own +// implementations of different CLI commands into the common execution +// framework +// +type CLICommand interface { + // Initialize allows a caller-defined command to plug itself into CLI + // argument parsing + Initialize(*kingpin.Application, *service.Config) + + // TryRun is executed after the CLI parsing is done. The command must + // determine if selectedCommand belongs to it and return match=true + TryRun(selectedCommand string, c *auth.TunClient) (match bool, err error) +} + +func Run2(distribution string, commands []CLICommand) { + utils.InitLogger(utils.LoggingForCLI, logrus.WarnLevel) + + // app is the command line parser + app := utils.InitCLIParser("tctl", GlobalHelpString) + + // cfg (teleport auth server configuration) is going to be shared by all + // commands + cfg := service.MakeDefaultConfig() + + // each command will add itself to the CLI parser: + for i := range commands { + commands[i].Initialize(app, cfg) + } + + // these global flags apply to all commands + var ccf GlobalCLIFlags + app.Flag("debug", "Enable verbose logging to stderr"). + Short('d'). + BoolVar(&ccf.Debug) + app.Flag("config", fmt.Sprintf("Path to a configuration file [%v]", defaults.ConfigFilePath)). + Short('c'). + ExistingFileVar(&ccf.ConfigFile) + app.Flag("config-string", + "Base64 encoded configuration string").Hidden().Envar(defaults.ConfigEnvar).StringVar(&ccf.ConfigString) + + // "version" command is always available: + ver := app.Command("version", "Print the version.") + app.HelpFlag.Short('h') + + // parse CLI commands+flags: + selectedCmd, err := app.Parse(os.Args[1:]) + if err != nil { + utils.FatalError(err) + } + + // "version" command? + if selectedCmd == ver.FullCommand() { + utils.PrintVersion(distribution) + return + } + + // configure all commands with Teleport configuration (they share 'cfg') + applyConfig(&ccf, cfg) + + // connect to the auth sever: + client, err := connectToAuthService(cfg) + if err != nil { + utils.FatalError(err) + } + + // execute whatever is selected: + var match bool + for _, c := range commands { + match, err = c.TryRun(selectedCmd, client) + if err != nil { + utils.FatalError(err) + } + if match { + break + } + } +} + // Run() is the same as 'make'. It helps to share the code between different // "distributions" like OSS or Enterprise // @@ -51,8 +133,6 @@ func Run(distribution string) { // generate default tctl configuration: cfg := service.MakeDefaultConfig() - cmdUsers := UserCommand{config: cfg} - cmdNodes := NodeCommand{config: cfg} cmdAuth := AuthCommand{config: cfg} cmdTokens := TokenCommand{config: cfg} cmdGet := GetCommand{config: cfg} @@ -60,7 +140,7 @@ func Run(distribution string) { cmdDelete := DeleteCommand{config: cfg} // define global flags: - var ccf CLIConfig + var ccf GlobalCLIFlags app.Flag("debug", "Enable verbose logging to stderr"). Short('d'). BoolVar(&ccf.Debug) @@ -74,20 +154,6 @@ func Run(distribution string) { ver := app.Command("version", "Print the version.") app.HelpFlag.Short('h') - // user add command: - users := app.Command("users", "Manage users logins") - - userAdd := users.Command("add", "Generate an invitation token and print the signup URL") - userAdd.Arg("login", "Teleport user login").Required().StringVar(&cmdUsers.login) - userAdd.Arg("local-logins", "Local UNIX users this account can log in as [login]"). - Default("").StringVar(&cmdUsers.allowedLogins) - userAdd.Alias(AddUserHelp) - - userUpdate := users.Command("update", "Update properties for existing user").Hidden() - userUpdate.Arg("login", "Teleport user login").Required().StringVar(&cmdUsers.login) - userUpdate.Flag("set-roles", "Roles to assign to this user"). - Default("").StringVar(&cmdUsers.roles) - delete := app.Command("del", "Delete resources").Hidden() delete.Arg("resource", "Resource to delete").SetValue(&cmdDelete.ref) @@ -102,26 +168,6 @@ func Run(distribution string) { create := app.Command("create", "Create or update a resource").Hidden() create.Flag("filename", "resource definition file").Short('f').StringVar(&cmdCreate.filename) - // list users command - userList := users.Command("ls", "List all user accounts") - - // delete user command - userDelete := users.Command("del", "Deletes user accounts") - userDelete.Arg("logins", "Comma-separated list of user logins to delete"). - Required().StringVar(&cmdUsers.login) - - // add node command - nodes := app.Command("nodes", "Issue invites for other nodes to join the cluster") - nodeAdd := nodes.Command("add", "Generate an invitation token. Use it to add a new node to the Teleport cluster") - nodeAdd.Flag("roles", "Comma-separated list of roles for the new node to assume [node]").Default("node").StringVar(&cmdNodes.roles) - nodeAdd.Flag("ttl", "Time to live for a generated token").Default(defaults.ProvisioningTokenTTL.String()).DurationVar(&cmdNodes.ttl) - nodeAdd.Flag("count", "add count tokens and output JSON with the list").Hidden().Default("1").IntVar(&cmdNodes.count) - nodeAdd.Flag("format", "output format, 'text' or 'json'").Hidden().Default("text").StringVar(&cmdNodes.format) - nodeAdd.Alias(AddNodeHelp) - nodeList := nodes.Command("ls", "List all active SSH nodes within the cluster") - nodeList.Flag("namespace", "Namespace of the nodes").Default(defaults.Namespace).StringVar(&cmdNodes.namespace) - nodeList.Alias(ListNodesHelp) - // operations on invitation tokens tokens := app.Command("tokens", "List or revoke invitation tokens") tokenList := tokens.Command("ls", "List node and user invitation tokens") @@ -184,18 +230,7 @@ func Run(distribution string) { err = cmdCreate.Create(client) case delete.FullCommand(): err = cmdDelete.Delete(client) - case userAdd.FullCommand(): - err = cmdUsers.Add(client) - case userList.FullCommand(): - err = cmdUsers.List(client) - case userUpdate.FullCommand(): - err = cmdUsers.Update(client) - case userDelete.FullCommand(): - err = cmdUsers.Delete(client) - case nodeAdd.FullCommand(): - err = cmdNodes.Invite(client) - case nodeList.FullCommand(): - err = cmdNodes.ListActive(client) + case authExport.FullCommand(): err = cmdAuth.ExportAuthorities(client) case tokenList.FullCommand(): @@ -256,7 +291,7 @@ func connectToAuthService(cfg *service.Config) (client *auth.TunClient, err erro // applyConfig takes configuration values from the config file and applies // them to 'service.Config' object -func applyConfig(ccf *CLIConfig, cfg *service.Config) error { +func applyConfig(ccf *GlobalCLIFlags, cfg *service.Config) error { // load /etc/teleport.yaml and apply it's values: fileConf, err := config.ReadConfigFile(ccf.ConfigFile) if err != nil { diff --git a/tool/tctl/common/usage.go b/tool/tctl/common/usage.go index 7a3982b21e8..d62d27a596b 100644 --- a/tool/tctl/common/usage.go +++ b/tool/tctl/common/usage.go @@ -21,7 +21,7 @@ const ( AddUserHelp = `Notes: 1. tctl will generate a signup token and give you a URL to share with a user. - He will have to configure the mandatory 2nd facto auth and select a password. + A user will have to complete account creation by visiting the URL. 2. A Teleport user account is not the same as a local UNIX users on SSH nodes. You must assign a list of allowed local users for every Teleport login. @@ -30,7 +30,7 @@ Examples: > tctl users add joe admin,nginx - This creates a Teleport identity 'joe' who can login as 'admin' or 'nginx' + This creates a Teleport account 'joe' who can login as 'admin' or 'nginx' to any SSH node connected to this auth server. > tctl users add joe diff --git a/tool/tctl/common/user_command.go b/tool/tctl/common/user_command.go index d465d53b162..d27ef80bea1 100644 --- a/tool/tctl/common/user_command.go +++ b/tool/tctl/common/user_command.go @@ -23,6 +23,7 @@ import ( "strconv" "strings" + "github.com/gravitational/kingpin" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" @@ -32,12 +33,58 @@ import ( ) // UserCommand implements `tctl users` set of commands +// It implements CLICommand interface type UserCommand struct { config *service.Config login string allowedLogins string roles string identities []string + + userAdd *kingpin.CmdClause + userUpdate *kingpin.CmdClause + userList *kingpin.CmdClause + userDelete *kingpin.CmdClause +} + +// Initialize allows UserCommand to plug itself into the CLI parser +func (u *UserCommand) Initialize(app *kingpin.Application, config *service.Config) { + u.config = config + users := app.Command("users", "Manage user accounts") + + u.userAdd = users.Command("add", "Generate a user invitation token") + u.userAdd.Arg("account", "Teleport user account name").Required().StringVar(&u.login) + u.userAdd.Arg("local-logins", "Local UNIX users this account can log in as [login]"). + Default("").StringVar(&u.allowedLogins) + u.userAdd.Alias(AddUserHelp) + + u.userUpdate = users.Command("update", "Update properties for existing user").Hidden() + u.userUpdate.Arg("login", "Teleport user login").Required().StringVar(&u.login) + u.userUpdate.Flag("set-roles", "Roles to assign to this user"). + Default("").StringVar(&u.roles) + + u.userList = users.Command("ls", "List all user accounts") + + u.userDelete = users.Command("rm", "Deletes user accounts").Alias("del") + u.userDelete.Arg("logins", "Comma-separated list of user logins to delete"). + Required().StringVar(&u.login) +} + +// TryRun takes the CLI command as an argument (like "users add") and executes it. +func (u *UserCommand) TryRun(cmd string, client *auth.TunClient) (match bool, err error) { + switch cmd { + case u.userAdd.FullCommand(): + err = u.Add(client) + case u.userUpdate.FullCommand(): + err = u.Update(client) + case u.userList.FullCommand(): + err = u.List(client) + case u.userDelete.FullCommand(): + err = u.Delete(client) + default: + return false, nil + } + return true, trace.Wrap(err) } // Add creates a new sign-up token and prints a token URL to stdout.