From 38127d75b5f6fce86037c1db93082e806780f496 Mon Sep 17 00:00:00 2001 From: Ev Kontsevoy Date: Tue, 5 Sep 2017 22:13:40 -0700 Subject: [PATCH 1/3] Implemented -f (force) flag for tctl create `tctl create` used to create or update (AKA "upsert") resources. Now there's a difference: `--force, -f` flag, if not set, means "create only". Otherwise it means "update". This means you can fail updating non-existing resources. --- lib/auth/apiserver.go | 1 + tool/tctl/common/resource_command.go | 203 +++++++++++++++++---------- 2 files changed, 131 insertions(+), 73 deletions(-) diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index cce00b2dbe41f..7d197d5199afa 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -611,6 +611,7 @@ func (s *APIServer) upsertUser(auth ClientI, w http.ResponseWriter, r *http.Requ if err != nil { return nil, trace.Wrap(err) } + err = auth.UpsertUser(user) if err != nil { return nil, trace.Wrap(err) diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index d87e5be8dd24c..dc9d50fb04492 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -19,7 +19,6 @@ package common import ( "fmt" "io" - "io/ioutil" "os" "github.com/gravitational/kingpin" @@ -32,13 +31,6 @@ import ( kyaml "k8s.io/apimachinery/pkg/util/yaml" ) -// ResourceCommandImpl allows to customize the implementation of certain -// subcommands of the resource command -type ResourceCommandImpl struct { - Delete func(services.Ref, *auth.TunClient) error - Create func(*services.UnknownResource, *auth.TunClient) error -} - // ResourceCommand implements `tctl get/create/list` commands for manipulating // Teleport resources type ResourceCommand struct { @@ -47,6 +39,7 @@ type ResourceCommand struct { format string namespace string withSecrets bool + force bool // filename is the name of the resource, used for 'create' filename string @@ -55,9 +48,6 @@ type ResourceCommand struct { delete *kingpin.CmdClause get *kingpin.CmdClause create *kingpin.CmdClause - - // customized implementation - Impl *ResourceCommandImpl } const getHelp = `Examples: @@ -74,8 +64,9 @@ Same as above, but using JSON output: func (g *ResourceCommand) Initialize(app *kingpin.Application, config *service.Config) { g.config = config - g.create = app.Command("create", "Create or update a resource") - g.create.Flag("filename", "resource definition file").Short('f').StringVar(&g.filename) + g.create = app.Command("create", "Create or update a Teleport resource from a YAML file") + g.create.Arg("filename", "resource definition file").Required().StringVar(&g.filename) + g.create.Flag("force", "Overwrite the resource if already exists").Short('f').BoolVar(&g.force) g.delete = app.Command("rm", "Delete a resource").Alias("del") g.delete.Arg("resource", "Resource to delete").SetValue(&g.ref) @@ -130,15 +121,9 @@ func (g *ResourceCommand) Get(client *auth.TunClient) error { // Create updates or insterts one or many resources func (u *ResourceCommand) Create(client *auth.TunClient) error { - var reader io.ReadCloser - var err error - if u.filename != "" { - reader, err = utils.OpenFile(u.filename) - if err != nil { - return trace.Wrap(err) - } - } else { - reader = ioutil.NopCloser(os.Stdin) + reader, err := utils.OpenFile(u.filename) + if err != nil { + return trace.Wrap(err) } decoder := kyaml.NewYAMLOrJSONDecoder(reader, 32*1024) count := 0 @@ -155,62 +140,129 @@ func (u *ResourceCommand) Create(client *auth.TunClient) error { return trace.Wrap(err) } count += 1 + switch raw.Kind { - case services.KindSAMLConnector: - conn, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := conn.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) - } - if err := client.UpsertSAMLConnector(conn); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created SAML connector: %v\n", conn.GetName()) - case services.KindOIDCConnector: - conn, err := services.GetOIDCConnectorMarshaler().UnmarshalOIDCConnector(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := client.UpsertOIDCConnector(conn); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created OIDC connector: %v\n", conn.GetName()) + case services.KindSAMLConnector, services.KindOIDCConnector: + return u.createConnector(client, raw) case services.KindUser: - user, err := services.GetUserMarshaler().UnmarshalUser(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := client.UpsertUser(user); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created user: %v\n", user.GetName()) + return u.createUser(client, raw) case services.KindTrustedCluster: - tc, err := services.GetTrustedClusterMarshaler().Unmarshal(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := client.UpsertTrustedCluster(tc); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created trusted cluster: %q\n", tc.GetName()) - case "": - return trace.BadParameter("missing resource kind") + return u.createTrustedCluster(client, raw) default: - // customized creation: - if u.Impl != nil && u.Impl.Create != nil { - err := u.Impl.Create(&raw, client) - if err != nil { - return trace.Wrap(err) - } - } else { - return trace.BadParameter("creating resources of type %q is not supported", raw.Kind) - } + return trace.BadParameter("creating resources of type %q is not supported", raw.Kind) } } } +// createTrustedCluster implements `tctl create cluster.yaml` command +func (u *ResourceCommand) createTrustedCluster(client *auth.TunClient, raw services.UnknownResource) error { + tc, err := services.GetTrustedClusterMarshaler().Unmarshal(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + + // check if such cluster already exists: + name := tc.GetName() + _, err = client.GetTrustedCluster(name) + if err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + } + exists := (err == nil) + if u.force == false && exists { + return trace.AlreadyExists("trusted cluster '%s' already exists", name) + } + if err := client.UpsertTrustedCluster(tc); err != nil { + return trace.Wrap(err) + } + fmt.Printf("trusted cluster '%s' has been %s\n", name, upsertVerb(exists)) + return nil +} + +// createConnector implements 'tctl create connector.yaml' command +func (u *ResourceCommand) createConnector(client *auth.TunClient, raw services.UnknownResource) error { + var ( + connectorName string + exists bool + err error + ) + switch raw.Kind { + + // SAML + case services.KindSAMLConnector: + conn, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + if err := conn.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + connectorName = conn.GetName() + _, err = client.GetSAMLConnector(connectorName, false) + if err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + } + exists = (err == nil) + if u.force == false && exists { + return trace.AlreadyExists("connector '%s' already exists", connectorName) + } + err = client.UpsertSAMLConnector(conn) + + // OpenID connect + case services.KindOIDCConnector: + conn, err := services.GetOIDCConnectorMarshaler().UnmarshalOIDCConnector(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + if err := conn.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + connectorName = conn.GetName() + _, err = client.GetOIDCConnector(connectorName, false) + if err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + } + exists = (err == nil) + if u.force == false && exists { + return trace.AlreadyExists("connector '%s' already exists", connectorName) + } + err = client.UpsertOIDCConnector(conn) + + // unknown connector type + default: + err = trace.BadParameter("unknown connector type: '%s'", raw.Kind) + } + + if err != nil { + return trace.Wrap(err) + } + fmt.Printf("authentication connector '%s' has been %s\n", connectorName, upsertVerb(exists)) + return nil +} + +// createUser implements 'tctl create user.yaml' command +func (u *ResourceCommand) createUser(client *auth.TunClient, raw services.UnknownResource) error { + user, err := services.GetUserMarshaler().UnmarshalUser(raw.Raw) + if err != nil { + return trace.Wrap(err) + } + userName := user.GetName() + // see if a user with such name exists: + _, err = client.GetUser(userName) + if err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + } + userExists := (err == nil) + // asked not to overwrite an existing user? check if it exists + if u.force == false && userExists { + return trace.AlreadyExists("user '%s' already exists", userName) + } + if err := client.UpsertUser(user); err != nil { + return trace.Wrap(err) + } + fmt.Printf("user '%s' has been %s\n", userName, upsertVerb(userExists)) + return nil +} + // Delete deletes resource by name func (d *ResourceCommand) Delete(client *auth.TunClient) (err error) { if d.ref.Kind == "" || d.ref.Name == "" { @@ -244,9 +296,6 @@ func (d *ResourceCommand) Delete(client *auth.TunClient) (err error) { } fmt.Printf("trusted cluster %q has been deleted\n", d.ref.Name) default: - if d.Impl != nil && d.Impl.Delete != nil { - return trace.Wrap(d.Impl.Delete(d.ref, client)) - } return trace.BadParameter("deleting resoruces of type %q is not supported", d.ref.Kind) } return nil @@ -370,3 +419,11 @@ const ( formatText = "text" formatJSON = "json" ) + +func upsertVerb(exists bool) string { + if exists { + return "updated" + } else { + return "created" + } +} From cc6350298753ff81a41b0c6bb2e7c6b2c1d3dea3 Mon Sep 17 00:00:00 2001 From: Ev Kontsevoy Date: Tue, 5 Sep 2017 22:36:31 -0700 Subject: [PATCH 2/3] Fixed the build (merged lost commit) --- e | 2 +- tool/tctl/common/user_command.go | 43 +++++++++++++------------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/e b/e index 677450a4f0fd3..077b96d03debe 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit 677450a4f0fd3fd88607c9b1ab7200582bd3ae98 +Subproject commit 077b96d03debeaace16b1366555753261d449356 diff --git a/tool/tctl/common/user_command.go b/tool/tctl/common/user_command.go index 5dead77b6adce..d2fbcab8211ef 100644 --- a/tool/tctl/common/user_command.go +++ b/tool/tctl/common/user_command.go @@ -46,16 +46,6 @@ type UserCommand struct { userUpdate *kingpin.CmdClause userList *kingpin.CmdClause userDelete *kingpin.CmdClause - - // behavior of the user command can be changed by providing - // different implementations of subcommands commands - Impl *UserCommandImpl -} - -// UserCommandImpl is used to supply alternative implementations of -// user subcommands -type UserCommandImpl struct { - List func([]services.User, *auth.TunClient) error } // Initialize allows UserCommand to plug itself into the CLI parser @@ -113,25 +103,30 @@ func (u *UserCommand) Add(client *auth.TunClient) error { if err != nil { return err } - proxies, err := client.GetProxies() - if err != nil { - return trace.Wrap(err) - } - hostname := "teleport-proxy" - if len(proxies) == 0 { - fmt.Printf("\x1b[1mWARNING\x1b[0m: this Teleport cluster does not have any proxy servers online.\nYou need to start some to be able to login.\n\n") - } else { - hostname = proxies[0].GetHostname() - } // try to auto-suggest the activation link + u.PrintSignupURL(client, token) + return nil +} + +func (u *UserCommand) PrintSignupURL(client *auth.TunClient, token string) { + hostname := "your.teleport.proxy" + + proxies, err := client.GetProxies() + if err == nil { + if len(proxies) == 0 { + fmt.Printf("\x1b[1mWARNING\x1b[0m: this Teleport cluster does not have any proxy servers online.\nYou need to start some to be able to login.\n\n") + } else { + hostname = proxies[0].GetHostname() + } + } _, proxyPort, err := net.SplitHostPort(u.config.Proxy.WebAddr.Addr) if err != nil { proxyPort = strconv.Itoa(defaults.HTTPListenPort) } url := web.CreateSignupLink(net.JoinHostPort(hostname, proxyPort), token) - fmt.Printf("Signup token has been created and is valid for %v seconds. Share this URL with the user:\n%v\n\nNOTE: make sure '%s' is accessible!\n", defaults.MaxSignupTokenTTL.Seconds(), url, hostname) - return nil + fmt.Printf("Signup token has been created and is valid for %v seconds. Share this URL with the user:\n%v\n\nNOTE: make sure '%s' is accessible!\n", + defaults.MaxSignupTokenTTL.Seconds(), url, hostname) } // Update updates existing user @@ -164,10 +159,6 @@ func (u *UserCommand) List(client *auth.TunClient) error { fmt.Println("No users found") return nil } - // see if "user ls" command is customized - if u.Impl != nil && u.Impl.List != nil { - return u.Impl.List(users, client) - } t := goterm.NewTable(0, 10, 5, ' ', 0) PrintHeader(t, []string{"User", "Allowed logins"}) for _, u := range users { From 9604d8661ee49e59fcd70ea5f00ae9cf3bd12938 Mon Sep 17 00:00:00 2001 From: Ev Kontsevoy Date: Tue, 5 Sep 2017 23:24:38 -0700 Subject: [PATCH 3/3] Updated to the latest enterprise version --- e | 2 +- tool/tctl/common/resource_command.go | 138 ++++++++++----------------- 2 files changed, 50 insertions(+), 90 deletions(-) diff --git a/e b/e index 077b96d03debe..9135398781efd 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit 077b96d03debeaace16b1366555753261d449356 +Subproject commit 9135398781efd8c6b39a1ced4689ead97d2bc0d9 diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index dc9d50fb04492..c2a494794db72 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -31,6 +31,9 @@ import ( kyaml "k8s.io/apimachinery/pkg/util/yaml" ) +type ResourceCreateHandler func(*auth.TunClient, services.UnknownResource) error +type ResourceKind string + // ResourceCommand implements `tctl get/create/list` commands for manipulating // Teleport resources type ResourceCommand struct { @@ -45,9 +48,11 @@ type ResourceCommand struct { filename string // CLI subcommands: - delete *kingpin.CmdClause - get *kingpin.CmdClause - create *kingpin.CmdClause + deleteCmd *kingpin.CmdClause + getCmd *kingpin.CmdClause + createCmd *kingpin.CmdClause + + CreateHandlers map[ResourceKind]ResourceCreateHandler } const getHelp = `Examples: @@ -62,22 +67,26 @@ Same as above, but using JSON output: // Initialize allows ResourceCommand to plug itself into the CLI parser func (g *ResourceCommand) Initialize(app *kingpin.Application, config *service.Config) { + g.CreateHandlers = map[ResourceKind]ResourceCreateHandler{ + services.KindUser: g.createUser, + services.KindTrustedCluster: g.createTrustedCluster, + } g.config = config - g.create = app.Command("create", "Create or update a Teleport resource from a YAML file") - g.create.Arg("filename", "resource definition file").Required().StringVar(&g.filename) - g.create.Flag("force", "Overwrite the resource if already exists").Short('f').BoolVar(&g.force) + g.createCmd = app.Command("create", "Create or update a Teleport resource from a YAML file") + g.createCmd.Arg("filename", "resource definition file").Required().StringVar(&g.filename) + g.createCmd.Flag("force", "Overwrite the resource if already exists").Short('f').BoolVar(&g.force) - g.delete = app.Command("rm", "Delete a resource").Alias("del") - g.delete.Arg("resource", "Resource to delete").SetValue(&g.ref) + g.deleteCmd = app.Command("rm", "Delete a resource").Alias("del") + g.deleteCmd.Arg("resource", "Resource to delete").SetValue(&g.ref) - g.get = app.Command("get", "Print a YAML declaration of various Teleport resources") - g.get.Arg("resource", "Resource spec: 'type/[name]'").SetValue(&g.ref) - g.get.Flag("format", "Output format: 'yaml' or 'json'").Default(formatYAML).StringVar(&g.format) - g.get.Flag("namespace", "Namespace of the resources").Hidden().Default(defaults.Namespace).StringVar(&g.namespace) - g.get.Flag("with-secrets", "Include secrets in resources like certificate authorities or OIDC connectors").Default("false").BoolVar(&g.withSecrets) + g.getCmd = app.Command("get", "Print a YAML declaration of various Teleport resources") + g.getCmd.Arg("resource", "Resource spec: 'type/[name]'").SetValue(&g.ref) + g.getCmd.Flag("format", "Output format: 'yaml' or 'json'").Default(formatYAML).StringVar(&g.format) + g.getCmd.Flag("namespace", "Namespace of the resources").Hidden().Default(defaults.Namespace).StringVar(&g.namespace) + g.getCmd.Flag("with-secrets", "Include secrets in resources like certificate authorities or OIDC connectors").Default("false").BoolVar(&g.withSecrets) - g.get.Alias(getHelp) + g.getCmd.Alias(getHelp) } // TryRun takes the CLI command as an argument (like "auth gen") and executes it @@ -85,13 +94,13 @@ func (g *ResourceCommand) Initialize(app *kingpin.Application, config *service.C func (g *ResourceCommand) TryRun(cmd string, client *auth.TunClient) (match bool, err error) { switch cmd { // tctl get - case g.get.FullCommand(): + case g.getCmd.FullCommand(): err = g.Get(client) // tctl create - case g.create.FullCommand(): + case g.createCmd.FullCommand(): err = g.Create(client) // tctl rm - case g.delete.FullCommand(): + case g.deleteCmd.FullCommand(): err = g.Delete(client) default: return false, nil @@ -99,6 +108,17 @@ func (g *ResourceCommand) TryRun(cmd string, client *auth.TunClient) (match bool return true, trace.Wrap(err) } +// IsDeleteSubcommand returns 'true' if the given command is `tctl rm` +func (g *ResourceCommand) IsDeleteSubcommand(cmd string) bool { + return cmd == g.deleteCmd.FullCommand() +} + +// GetRef returns the reference (basically type/name pair) of the resource +// the command is operating on +func (g *ResourceCommand) GetRef() services.Ref { + return g.ref +} + // Get prints one or many resources of a certain type func (g *ResourceCommand) Get(client *auth.TunClient) error { collection, err := g.getCollection(client) @@ -141,16 +161,12 @@ func (u *ResourceCommand) Create(client *auth.TunClient) error { } count += 1 - switch raw.Kind { - case services.KindSAMLConnector, services.KindOIDCConnector: - return u.createConnector(client, raw) - case services.KindUser: - return u.createUser(client, raw) - case services.KindTrustedCluster: - return u.createTrustedCluster(client, raw) - default: + // locate the creator function for a given resource kind: + creator, found := u.CreateHandlers[ResourceKind(raw.Kind)] + if !found { return trace.BadParameter("creating resources of type %q is not supported", raw.Kind) } + return creator(client, raw) } } @@ -174,68 +190,7 @@ func (u *ResourceCommand) createTrustedCluster(client *auth.TunClient, raw servi if err := client.UpsertTrustedCluster(tc); err != nil { return trace.Wrap(err) } - fmt.Printf("trusted cluster '%s' has been %s\n", name, upsertVerb(exists)) - return nil -} - -// createConnector implements 'tctl create connector.yaml' command -func (u *ResourceCommand) createConnector(client *auth.TunClient, raw services.UnknownResource) error { - var ( - connectorName string - exists bool - err error - ) - switch raw.Kind { - - // SAML - case services.KindSAMLConnector: - conn, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := conn.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) - } - connectorName = conn.GetName() - _, err = client.GetSAMLConnector(connectorName, false) - if err != nil && !trace.IsNotFound(err) { - return trace.Wrap(err) - } - exists = (err == nil) - if u.force == false && exists { - return trace.AlreadyExists("connector '%s' already exists", connectorName) - } - err = client.UpsertSAMLConnector(conn) - - // OpenID connect - case services.KindOIDCConnector: - conn, err := services.GetOIDCConnectorMarshaler().UnmarshalOIDCConnector(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := conn.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) - } - connectorName = conn.GetName() - _, err = client.GetOIDCConnector(connectorName, false) - if err != nil && !trace.IsNotFound(err) { - return trace.Wrap(err) - } - exists = (err == nil) - if u.force == false && exists { - return trace.AlreadyExists("connector '%s' already exists", connectorName) - } - err = client.UpsertOIDCConnector(conn) - - // unknown connector type - default: - err = trace.BadParameter("unknown connector type: '%s'", raw.Kind) - } - - if err != nil { - return trace.Wrap(err) - } - fmt.Printf("authentication connector '%s' has been %s\n", connectorName, upsertVerb(exists)) + fmt.Printf("trusted cluster '%s' has been %s\n", name, UpsertVerb(exists)) return nil } @@ -259,7 +214,7 @@ func (u *ResourceCommand) createUser(client *auth.TunClient, raw services.Unknow if err := client.UpsertUser(user); err != nil { return trace.Wrap(err) } - fmt.Printf("user '%s' has been %s\n", userName, upsertVerb(userExists)) + fmt.Printf("user '%s' has been %s\n", userName, UpsertVerb(userExists)) return nil } @@ -301,6 +256,11 @@ func (d *ResourceCommand) Delete(client *auth.TunClient) (err error) { return nil } +// IsForced returns true if -f flag was passed +func (cmd *ResourceCommand) IsForced() bool { + return cmd.force +} + func (g *ResourceCommand) getCollection(client auth.ClientI) (c ResourceCollection, err error) { if g.ref.Kind == "" { return nil, trace.BadParameter("specify resource to list, e.g. 'tctl get roles'") @@ -420,7 +380,7 @@ const ( formatJSON = "json" ) -func upsertVerb(exists bool) string { +func UpsertVerb(exists bool) string { if exists { return "updated" } else {