Merge pull request #1266 from gravitational/ev/force

`tctl create` improvements
This commit is contained in:
Ev Kontsevoy 2017-09-06 10:24:43 -07:00 committed by GitHub
commit aeaa64bd85
4 changed files with 125 additions and 116 deletions

2
e

@ -1 +1 @@
Subproject commit 677450a4f0fd3fd88607c9b1ab7200582bd3ae98
Subproject commit 9135398781efd8c6b39a1ced4689ead97d2bc0d9

View file

@ -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)

View file

@ -19,7 +19,6 @@ package common
import (
"fmt"
"io"
"io/ioutil"
"os"
"github.com/gravitational/kingpin"
@ -32,12 +31,8 @@ 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
}
type ResourceCreateHandler func(*auth.TunClient, services.UnknownResource) error
type ResourceKind string
// ResourceCommand implements `tctl get/create/list` commands for manipulating
// Teleport resources
@ -47,17 +42,17 @@ type ResourceCommand struct {
format string
namespace string
withSecrets bool
force bool
// filename is the name of the resource, used for 'create'
filename string
// CLI subcommands:
delete *kingpin.CmdClause
get *kingpin.CmdClause
create *kingpin.CmdClause
deleteCmd *kingpin.CmdClause
getCmd *kingpin.CmdClause
createCmd *kingpin.CmdClause
// customized implementation
Impl *ResourceCommandImpl
CreateHandlers map[ResourceKind]ResourceCreateHandler
}
const getHelp = `Examples:
@ -72,21 +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 resource")
g.create.Flag("filename", "resource definition file").Short('f').StringVar(&g.filename)
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
@ -94,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
@ -108,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)
@ -130,15 +141,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 +160,64 @@ 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.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())
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")
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)
}
// 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)
}
}
// 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
}
// 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,14 +251,16 @@ 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
}
// 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'")
@ -370,3 +379,11 @@ const (
formatText = "text"
formatJSON = "json"
)
func UpsertVerb(exists bool) string {
if exists {
return "updated"
} else {
return "created"
}
}

View file

@ -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 {