diff --git a/cmd/cluster.go b/cmd/cluster.go index 3e29a209..bf8cc922 100644 --- a/cmd/cluster.go +++ b/cmd/cluster.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/rancher/cli/cliclient" + "github.com/rancher/norman/types" managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -98,18 +99,26 @@ func ClusterCommand() cli.Command { Name: "import", Usage: "Import an existing Kubernetes cluster into a Rancher cluster", Description: importDescription, - ArgsUsage: "[CLUSTERID CLUSTERNAME]", + ArgsUsage: "CLUSTERID|--cluster-name CLUSTERNAME", Action: clusterImport, Flags: []cli.Flag{ + cli.StringFlag{ + Name: "cluster-name, name, n", + Usage: "Specify cluster by `CLUSTERNAME` instead of CLUSTERID", + }, quietFlag, }, }, { Name: "add-node", Usage: "Outputs the docker command needed to add a node to an existing Rancher custom cluster", - ArgsUsage: "[CLUSTERID CLUSTERNAME]", + ArgsUsage: "CLUSTERID|--cluster-name CLUSTERNAME", Action: clusterAddNode, Flags: []cli.Flag{ + cli.StringFlag{ + Name: "cluster-name, name, n", + Usage: "Specify cluster by `CLUSTERNAME` instead of CLUSTERID", + }, cli.StringSliceFlag{ Name: "label", Usage: "Label to apply to a node in the format [name]=[value]", @@ -136,22 +145,40 @@ func ClusterCommand() cli.Command { { Name: "delete", Aliases: []string{"rm"}, - Usage: "Delete a cluster", - ArgsUsage: "[CLUSTERID/CLUSTERNAME...]", + Usage: "Delete one or more clusters", + ArgsUsage: "CLUSTERID|--cluster-name CLUSTERNAME", Action: clusterDelete, + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "cluster-name, name, n", + Usage: "Specify cluster by `CLUSTERNAME` instead of CLUSTERID. Supply multiple times for multiple cluster names.", + }, + }, }, { Name: "export", Usage: "Export a cluster", - ArgsUsage: "[CLUSTERID/CLUSTERNAME...]", + ArgsUsage: "CLUSTERID|--cluster-name CLUSTERNAME", Action: clusterExport, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "cluster-name, name, n", + Usage: "Specify cluster by `CLUSTERNAME` instead of CLUSTERID", + }, + }, }, { Name: "kubeconfig", Aliases: []string{"kf"}, - Usage: "Return the kube config used to access the cluster", - ArgsUsage: "[CLUSTERID CLUSTERNAME]", + Usage: "Return the kubeconfig used to access the cluster", + ArgsUsage: "CLUSTERID|--cluster-name CLUSTERNAME", Action: clusterKubeConfig, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "cluster-name, name, n", + Usage: "Specify cluster by `CLUSTERNAME` instead of CLUSTERID", + }, + }, }, { Name: "add-member-role", @@ -284,7 +311,7 @@ func clusterCreate(ctx *cli.Context) error { } func clusterImport(ctx *cli.Context) error { - if ctx.NArg() == 0 { + if ctx.NArg() == 0 && ctx.String("cluster-name") == "" { return cli.ShowSubcommandHelp(ctx) } @@ -293,7 +320,12 @@ func clusterImport(ctx *cli.Context) error { return err } - resource, err := Lookup(c, ctx.Args().First(), "cluster") + var resource *types.Resource + if name := ctx.String("cluster-name"); name != "" { + resource, err = LookupByName(c, name, "cluster") + } else { + resource, err = LookupByID(c, ctx.Args().First(), "cluster") + } if err != nil { return err } @@ -325,7 +357,7 @@ func clusterImport(ctx *cli.Context) error { // clusterAddNode prints the command needed to add a node to a cluster func clusterAddNode(ctx *cli.Context) error { - if ctx.NArg() == 0 { + if ctx.NArg() == 0 && ctx.String("cluster-name") == "" { return cli.ShowSubcommandHelp(ctx) } @@ -334,7 +366,12 @@ func clusterAddNode(ctx *cli.Context) error { return err } - resource, err := Lookup(c, ctx.Args().First(), "cluster") + var resource *types.Resource + if name := ctx.String("cluster-name"); name != "" { + resource, err = LookupByName(c, name, "cluster") + } else { + resource, err = LookupByID(c, ctx.Args().First(), "cluster") + } if err != nil { return err } @@ -400,18 +437,43 @@ func clusterAddNode(ctx *cli.Context) error { } func clusterDelete(ctx *cli.Context) error { - if ctx.NArg() == 0 { + if ctx.NArg() == 0 && len(ctx.StringSlice("cluster-name")) == 0 { return cli.ShowSubcommandHelp(ctx) } + clusterNames := ctx.StringSlice("cluster-name") + clusterIDs := ctx.Args() + c, err := GetClient(ctx) if err != nil { return err } + schemaclient, err := lookupSchemaClient(c, "cluster") + if err != nil { + return err + } + + for _, clusterID := range clusterIDs { + + resource, err := lookupByIDWithClient(schemaclient, clusterID, "cluster") + if err != nil { + return err + } + + cluster, err := getClusterByID(c, resource.ID) + if err != nil { + return err + } - for _, cluster := range ctx.Args() { + err = c.ManagementClient.Cluster.Delete(cluster) + if err != nil { + return err + } + fmt.Printf("Cluster with ID %s deleted\n", clusterID) + } - resource, err := Lookup(c, cluster, "cluster") + for _, clusterName := range clusterNames { + resource, err := lookupByNameWithClient(schemaclient, clusterName, "cluster") if err != nil { return err } @@ -425,13 +487,14 @@ func clusterDelete(ctx *cli.Context) error { if err != nil { return err } + fmt.Printf("Cluster with name %s deleted\n", clusterName) } return nil } func clusterExport(ctx *cli.Context) error { - if ctx.NArg() == 0 { + if ctx.NArg() == 0 && ctx.String("cluster-name") == "" { return cli.ShowSubcommandHelp(ctx) } @@ -440,7 +503,12 @@ func clusterExport(ctx *cli.Context) error { return err } - resource, err := Lookup(c, ctx.Args().First(), "cluster") + var resource *types.Resource + if name := ctx.String("cluster-name"); name != "" { + resource, err = LookupByName(c, name, "cluster") + } else { + resource, err = LookupByID(c, ctx.Args().First(), "cluster") + } if err != nil { return err } @@ -464,7 +532,7 @@ func clusterExport(ctx *cli.Context) error { } func clusterKubeConfig(ctx *cli.Context) error { - if ctx.NArg() == 0 { + if ctx.NArg() == 0 && ctx.String("cluster-name") == "" { return cli.ShowSubcommandHelp(ctx) } @@ -473,7 +541,12 @@ func clusterKubeConfig(ctx *cli.Context) error { return err } - resource, err := Lookup(c, ctx.Args().First(), "cluster") + var resource *types.Resource + if name := ctx.String("cluster-name"); name != "" { + resource, err = LookupByName(c, name, "cluster") + } else { + resource, err = LookupByID(c, ctx.Args().First(), "cluster") + } if err != nil { return err } diff --git a/cmd/common.go b/cmd/common.go index e043a8f5..35b3a1cd 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -11,6 +11,7 @@ import ( "io" "io/ioutil" "math/rand" + "net/http" "net/url" "os" "os/exec" @@ -44,7 +45,8 @@ const ( ) var ( - errNoURL = errors.New("RANCHER_URL environment or --Url is not set, run `login`") + errNoURL = errors.New("RANCHER_URL environment or --Url is not set, run `login`") + errNotFound = errors.New("not found") // ManagementResourceTypes lists the types we use the management client for ManagementResourceTypes = []string{"cluster", "node", "project"} // ProjectResourceTypes lists the types we use the cluster client for @@ -344,92 +346,157 @@ func GetResourceType(c *cliclient.MasterClient, resource string) (string, error) return "", fmt.Errorf("unknown resource type: %s", resource) } +// Lookup looks up a resource by ID first, and then by name if it's not found by ID. func Lookup(c *cliclient.MasterClient, name string, types ...string) (*ntypes.Resource, error) { var byName *ntypes.Resource for _, schemaType := range types { - rt, err := GetResourceType(c, schemaType) + schemaClient, err := lookupSchemaClient(c, schemaType) if err != nil { - logrus.Debugf("Error GetResourceType: %v", err) return nil, err } - var schemaClient clientbase.APIBaseClientInterface - // the schemaType dictates which client we need to use - if c.CAPIClient != nil { - if strings.HasPrefix(rt, "cluster.x-k8s.io") { - schemaClient = c.CAPIClient - } - } - if c.ManagementClient != nil { - if _, ok := c.ManagementClient.APIBaseClient.Types[rt]; ok { - schemaClient = c.ManagementClient - } - } - if c.ProjectClient != nil { - if _, ok := c.ProjectClient.APIBaseClient.Types[rt]; ok { - schemaClient = c.ProjectClient - } - } - if c.ClusterClient != nil { - if _, ok := c.ClusterClient.APIBaseClient.Types[rt]; ok { - schemaClient = c.ClusterClient - } - } - // Attempt to get the resource by ID - var resource ntypes.Resource - - if err := schemaClient.ByID(schemaType, name, &resource); !clientbase.IsNotFound(err) && err != nil { - logrus.Debugf("Error schemaClient.ByID: %v", err) + resource, err := lookupByIDWithClient(schemaClient, name, schemaType) + if err != nil && !errors.Is(err, errNotFound) { return nil, err - } else if err == nil && resource.ID == name { - return &resource, nil } - - // Resource was not found assuming the ID, check if it's the name of a resource - var collection ntypes.ResourceCollection - - listOpts := &ntypes.ListOpts{ - Filters: map[string]interface{}{ - "name": name, - "removed_null": 1, - }, + if resource != nil { + return resource, nil } - if err := schemaClient.List(schemaType, listOpts, &collection); !clientbase.IsNotFound(err) && err != nil { - logrus.Debugf("Error schemaClient.List: %v", err) + // Resource was not found assuming the ID, check if it's the name of a resource + resource, err = lookupByNameWithClient(schemaClient, name, schemaType) + if err != nil && !errors.Is(err, errNotFound) { return nil, err } - if len(collection.Data) > 1 { - ids := []string{} - for _, data := range collection.Data { - ids = append(ids, data.ID) - } - return nil, fmt.Errorf("Multiple resources of type %s found for name %s: %v", schemaType, name, ids) - } - // No matches for this schemaType, try the next one - if len(collection.Data) == 0 { + if resource == nil { continue } if byName != nil { - return nil, fmt.Errorf("Multiple resources named %s: %s:%s, %s:%s", name, collection.Data[0].Type, - collection.Data[0].ID, byName.Type, byName.ID) + return nil, fmt.Errorf("multiple resources named %s: %s:%s, %s:%s", name, resource.Type, + resource.ID, byName.Type, byName.ID) } - byName = &collection.Data[0] + byName = resource } if byName == nil { - return nil, fmt.Errorf("Not found: %s", name) + return nil, fmt.Errorf("resource %s: %w", name, errNotFound) } return byName, nil } +func lookupSchemaClient(c *cliclient.MasterClient, schemaType string) (clientbase.APIBaseClientInterface, error) { + rt, err := GetResourceType(c, schemaType) + if err != nil { + logrus.Debugf("Error GetResourceType: %v", err) + return nil, err + } + var schemaClient clientbase.APIBaseClientInterface + // the schemaType dictates which client we need to use + if c.CAPIClient != nil { + if strings.HasPrefix(rt, "cluster.x-k8s.io") { + schemaClient = c.CAPIClient + } + } + if c.ManagementClient != nil { + if _, ok := c.ManagementClient.APIBaseClient.Types[rt]; ok { + schemaClient = c.ManagementClient + } + } + if c.ProjectClient != nil { + if _, ok := c.ProjectClient.APIBaseClient.Types[rt]; ok { + schemaClient = c.ProjectClient + } + } + if c.ClusterClient != nil { + if _, ok := c.ClusterClient.APIBaseClient.Types[rt]; ok { + schemaClient = c.ClusterClient + } + } + return schemaClient, nil +} + +// LookupByName looks up a resource by name +func LookupByName(c *cliclient.MasterClient, name, schemaType string) (*ntypes.Resource, error) { + schemaClient, err := lookupSchemaClient(c, schemaType) + if err != nil { + return nil, err + } + return lookupByNameWithClient(schemaClient, name, schemaType) +} + +func lookupByNameWithClient(schemaClient clientbase.APIBaseClientInterface, name, schemaType string) (*ntypes.Resource, error) { + var collection ntypes.ResourceCollection + + listOpts := &ntypes.ListOpts{ + Filters: map[string]interface{}{ + "name": name, + "removed_null": 1, + }, + } + + if err := schemaClient.List(schemaType, listOpts, &collection); err != nil { + logrus.Debugf("Error schemaClient.List: %v", err) + if clientbase.IsNotFound(err) { + return nil, fmt.Errorf("resource of type %s with name %s: %w", schemaType, name, errNotFound) + } + // prevents 403 from falsely returning as an unhandled error + if apiError, ok := err.(*clientbase.APIError); ok { + if apiError.StatusCode == http.StatusForbidden { + return nil, fmt.Errorf("resource of type %s with name %s: %w", schemaType, name, errNotFound) + } + } + return nil, err + } + + if len(collection.Data) > 1 { + ids := []string{} + for _, data := range collection.Data { + ids = append(ids, data.ID) + } + return nil, fmt.Errorf("multiple resources of type %s found for name %s: %v", schemaType, name, ids) + } + if len(collection.Data) == 0 { + return nil, fmt.Errorf("resource of type %s with name %s: %w", schemaType, name, errNotFound) + } + + return &collection.Data[0], nil +} + +// LookupByID looks up a resource by it's ID +func LookupByID(c *cliclient.MasterClient, id, schemaType string) (*ntypes.Resource, error) { + schemaClient, err := lookupSchemaClient(c, schemaType) + if err != nil { + return nil, err + } + return lookupByIDWithClient(schemaClient, id, schemaType) +} + +func lookupByIDWithClient(schemaClient clientbase.APIBaseClientInterface, id, schemaType string) (*ntypes.Resource, error) { + var resource ntypes.Resource + + if err := schemaClient.ByID(schemaType, id, &resource); err != nil { + logrus.Debugf("Error schemaClient.ByID: %v", err) + if clientbase.IsNotFound(err) { + return nil, fmt.Errorf("resource of type %s with ID %s: %w", schemaType, id, errNotFound) + } + // prevents 403 from falsely returning as an unhandled error + if apiError, ok := err.(*clientbase.APIError); ok { + if apiError.StatusCode == http.StatusForbidden { + return nil, fmt.Errorf("resource of type %s with ID %s: %w", schemaType, id, errNotFound) + } + } + return nil, err + } + return &resource, nil +} + func RandomName() string { return strings.Replace(namesgenerator.GetRandomName(0), "_", "-", -1) }