github.com/pluralsh/plural-cli@v0.9.5/cmd/plural/cd_clusters.go (about)

     1  package plural
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/AlecAivazis/survey/v2"
     8  	gqlclient "github.com/pluralsh/console-client-go"
     9  	"github.com/pluralsh/plural-cli/pkg/cd"
    10  	"github.com/pluralsh/plural-cli/pkg/console"
    11  	"github.com/pluralsh/plural-cli/pkg/kubernetes/config"
    12  	"github.com/pluralsh/plural-cli/pkg/utils"
    13  	"github.com/pluralsh/polly/containers"
    14  	"github.com/samber/lo"
    15  	"github.com/urfave/cli"
    16  )
    17  
    18  var providerSurvey = []*survey.Question{
    19  	{
    20  		Name:   "name",
    21  		Prompt: &survey.Input{Message: "Enter the name of your provider:"},
    22  	},
    23  	{
    24  		Name:   "namespace",
    25  		Prompt: &survey.Input{Message: "Enter the namespace of your provider:"},
    26  	},
    27  }
    28  
    29  func (p *Plural) cdClusters() cli.Command {
    30  	return cli.Command{
    31  		Name:        "clusters",
    32  		Subcommands: p.cdClusterCommands(),
    33  		Usage:       "manage CD clusters",
    34  	}
    35  }
    36  
    37  func (p *Plural) cdClusterCommands() []cli.Command {
    38  	return []cli.Command{
    39  		{
    40  			Name:   "list",
    41  			Action: latestVersion(p.handleListClusters),
    42  			Usage:  "list clusters",
    43  		},
    44  		{
    45  			Name:      "describe",
    46  			Action:    latestVersion(requireArgs(p.handleDescribeCluster, []string{"CLUSTER_ID"})),
    47  			Usage:     "describe cluster",
    48  			ArgsUsage: "CLUSTER_ID",
    49  			Flags: []cli.Flag{
    50  				cli.StringFlag{Name: "o", Usage: "output format"},
    51  			},
    52  		},
    53  		{
    54  			Name:      "update",
    55  			Action:    latestVersion(requireArgs(p.handleUpdateCluster, []string{"CLUSTER_ID"})),
    56  			Usage:     "update cluster",
    57  			ArgsUsage: "CLUSTER_ID",
    58  			Flags: []cli.Flag{
    59  				cli.StringFlag{Name: "handle", Usage: "unique human readable name used to identify this cluster"},
    60  				cli.StringFlag{Name: "kubeconf-path", Usage: "path to kubeconfig"},
    61  				cli.StringFlag{Name: "kubeconf-context", Usage: "the kubeconfig context you want to use. If not specified, the current one will be used"},
    62  			},
    63  		},
    64  		{
    65  			Name:      "delete",
    66  			Action:    latestVersion(requireArgs(p.handleDeleteCluster, []string{"CLUSTER_ID"})),
    67  			Usage:     "deregisters a cluster in plural cd, and drains all services (unless --soft is specified)",
    68  			ArgsUsage: "CLUSTER_ID",
    69  			Flags: []cli.Flag{
    70  				cli.BoolFlag{
    71  					Name:  "soft",
    72  					Usage: "deletes a cluster in our system but doesn't drain resources, leaving them untouched",
    73  				},
    74  			},
    75  		},
    76  		{
    77  			Name:      "get-credentials",
    78  			Aliases:   []string{"kubeconfig"},
    79  			Action:    latestVersion(requireArgs(p.handleGetClusterCredentials, []string{"CLUSTER_ID"})),
    80  			Usage:     "updates kubeconfig file with appropriate credentials to point to specified cluster",
    81  			ArgsUsage: "CLUSTER_ID",
    82  		},
    83  		{
    84  			Name:      "create",
    85  			Action:    latestVersion(requireArgs(p.handleCreateCluster, []string{"CLUSTER_NAME"})),
    86  			Usage:     "create cluster",
    87  			ArgsUsage: "CLUSTER_NAME",
    88  			Flags: []cli.Flag{
    89  				cli.StringFlag{Name: "handle", Usage: "unique human readable name used to identify this cluster"},
    90  				cli.StringFlag{Name: "version", Usage: "kubernetes cluster version", Required: true},
    91  			},
    92  		},
    93  		{
    94  			Name:   "bootstrap",
    95  			Action: latestVersion(p.handleClusterBootstrap),
    96  			Usage:  "creates a new BYOK cluster and installs the agent onto it using the current kubeconfig",
    97  			Flags: []cli.Flag{
    98  				cli.StringFlag{Name: "name", Usage: "The name you'll give the cluster", Required: true},
    99  				cli.StringFlag{Name: "handle", Usage: "optional handle for the cluster"},
   100  				cli.StringFlag{Name: "values", Usage: "values file to use for the deployment agent helm chart", Required: false},
   101  				cli.StringSliceFlag{
   102  					Name:  "tag",
   103  					Usage: "a cluster tag to add, useful for targeting with global services",
   104  				},
   105  			},
   106  		},
   107  		{
   108  			Name:   "reinstall",
   109  			Action: latestVersion(p.handleClusterReinstall),
   110  			Flags: []cli.Flag{
   111  				cli.StringFlag{Name: "values", Usage: "values file to use for the deployment agent helm chart", Required: false},
   112  			},
   113  			Usage: "reinstalls the deployment operator into a cluster",
   114  		},
   115  	}
   116  }
   117  
   118  func (p *Plural) handleListClusters(_ *cli.Context) error {
   119  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   120  		return err
   121  	}
   122  
   123  	clusters, err := p.ConsoleClient.ListClusters()
   124  	if err != nil {
   125  		return err
   126  	}
   127  	if clusters == nil {
   128  		return fmt.Errorf("returned objects list [ListClusters] is nil")
   129  	}
   130  	headers := []string{"Id", "Name", "Handle", "Version", "Provider"}
   131  	return utils.PrintTable(clusters.Clusters.Edges, headers, func(cl *gqlclient.ClusterEdgeFragment) ([]string, error) {
   132  		provider := ""
   133  		if cl.Node.Provider != nil {
   134  			provider = cl.Node.Provider.Name
   135  		}
   136  		handle := ""
   137  		if cl.Node.Handle != nil {
   138  			handle = *cl.Node.Handle
   139  		}
   140  		version := ""
   141  		if cl.Node.Version != nil {
   142  			version = *cl.Node.Version
   143  		}
   144  		return []string{cl.Node.ID, cl.Node.Name, handle, version, provider}, nil
   145  	})
   146  }
   147  
   148  func (p *Plural) handleDescribeCluster(c *cli.Context) error {
   149  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   150  		return err
   151  	}
   152  	existing, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0)))
   153  	if err != nil {
   154  		return err
   155  	}
   156  	if existing == nil {
   157  		return fmt.Errorf("existing cluster is empty")
   158  	}
   159  	output := c.String("o")
   160  	if output == "json" {
   161  		utils.NewJsonPrinter(existing).PrettyPrint()
   162  	} else if output == "yaml" {
   163  		utils.NewYAMLPrinter(existing).PrettyPrint()
   164  	}
   165  	desc, err := console.DescribeCluster(existing)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	fmt.Print(desc)
   170  	return nil
   171  }
   172  
   173  func (p *Plural) handleUpdateCluster(c *cli.Context) error {
   174  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   175  		return err
   176  	}
   177  	existing, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0)))
   178  	if err != nil {
   179  		return err
   180  	}
   181  	if existing == nil {
   182  		return fmt.Errorf("this cluster does not exist")
   183  	}
   184  	updateAttr := gqlclient.ClusterUpdateAttributes{
   185  		Version: existing.Version,
   186  		Handle:  existing.Handle,
   187  	}
   188  	newHandle := c.String("handle")
   189  	if newHandle != "" {
   190  		updateAttr.Handle = &newHandle
   191  	}
   192  	kubeconfigPath := c.String("kubeconf-path")
   193  	if kubeconfigPath != "" {
   194  		kubeconfig, err := config.GetKubeconfig(kubeconfigPath, c.String("kubeconf-context"))
   195  		if err != nil {
   196  			return err
   197  		}
   198  
   199  		updateAttr.Kubeconfig = &gqlclient.KubeconfigAttributes{
   200  			Raw: &kubeconfig,
   201  		}
   202  	}
   203  
   204  	result, err := p.ConsoleClient.UpdateCluster(existing.ID, updateAttr)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	headers := []string{"Id", "Name", "Handle", "Version", "Provider"}
   209  	return utils.PrintTable([]gqlclient.ClusterFragment{*result.UpdateCluster}, headers, func(cl gqlclient.ClusterFragment) ([]string, error) {
   210  		provider := ""
   211  		if cl.Provider != nil {
   212  			provider = cl.Provider.Name
   213  		}
   214  		handle := ""
   215  		if cl.Handle != nil {
   216  			handle = *cl.Handle
   217  		}
   218  		return []string{cl.ID, cl.Name, handle, *cl.Version, provider}, nil
   219  	})
   220  }
   221  
   222  func (p *Plural) handleDeleteCluster(c *cli.Context) error {
   223  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   224  		return err
   225  	}
   226  
   227  	existing, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0)))
   228  	if err != nil {
   229  		return err
   230  	}
   231  	if existing == nil {
   232  		return fmt.Errorf("this cluster does not exist")
   233  	}
   234  
   235  	if c.Bool("soft") {
   236  		fmt.Println("detaching cluster from Plural CD, this will leave all workloads running.")
   237  		return p.ConsoleClient.DetachCluster(existing.ID)
   238  	}
   239  
   240  	return p.ConsoleClient.DeleteCluster(existing.ID)
   241  }
   242  
   243  func (p *Plural) handleGetClusterCredentials(c *cli.Context) error {
   244  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   245  		return err
   246  	}
   247  
   248  	cluster, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0)))
   249  	if err != nil {
   250  		return err
   251  	}
   252  	if cluster == nil {
   253  		return fmt.Errorf("cluster is nil")
   254  	}
   255  
   256  	return cd.SaveClusterKubeconfig(cluster, p.ConsoleClient.Token())
   257  }
   258  
   259  func (p *Plural) handleCreateCluster(c *cli.Context) error {
   260  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   261  		return err
   262  	}
   263  	name := c.Args().Get(0)
   264  	attr := gqlclient.ClusterAttributes{
   265  		Name: name,
   266  	}
   267  	if c.String("handle") != "" {
   268  		attr.Handle = lo.ToPtr(c.String("handle"))
   269  	}
   270  	if c.String("version") != "" {
   271  		attr.Version = lo.ToPtr(c.String("version"))
   272  	}
   273  
   274  	providerList, err := p.ConsoleClient.ListProviders()
   275  	if err != nil {
   276  		return err
   277  	}
   278  	providerNames := []string{}
   279  	providerMap := map[string]string{}
   280  	cloudProviders := []string{}
   281  	for _, prov := range providerList.ClusterProviders.Edges {
   282  		providerNames = append(providerNames, prov.Node.Name)
   283  		providerMap[prov.Node.Name] = prov.Node.ID
   284  		cloudProviders = append(cloudProviders, prov.Node.Cloud)
   285  	}
   286  
   287  	existingProv := containers.ToSet[string](cloudProviders)
   288  	availableProv := containers.ToSet[string](availableProviders)
   289  	toCreate := availableProv.Difference(existingProv)
   290  	createNewProvider := "Create New Provider"
   291  
   292  	if toCreate.Len() != 0 {
   293  		providerNames = append(providerNames, createNewProvider)
   294  	}
   295  
   296  	prompt := &survey.Select{
   297  		Message: "Select one of the following providers:",
   298  		Options: providerNames,
   299  	}
   300  	provider := ""
   301  	if err := survey.AskOne(prompt, &provider, survey.WithValidator(survey.Required)); err != nil {
   302  		return err
   303  	}
   304  	if provider != createNewProvider {
   305  		utils.Success("Using provider %s\n", provider)
   306  		id := providerMap[provider]
   307  		attr.ProviderID = &id
   308  	} else {
   309  
   310  		clusterProv, err := p.handleCreateProvider(toCreate.List())
   311  		if err != nil {
   312  			return err
   313  		}
   314  		if clusterProv == nil {
   315  			utils.Success("All supported providers are created\n")
   316  			return nil
   317  		}
   318  		utils.Success("Provider %s created successfully\n", clusterProv.CreateClusterProvider.Name)
   319  		attr.ProviderID = &clusterProv.CreateClusterProvider.ID
   320  		provider = clusterProv.CreateClusterProvider.Cloud
   321  	}
   322  
   323  	ca, err := cd.AskCloudSettings(provider)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	attr.CloudSettings = ca
   328  
   329  	existing, err := p.ConsoleClient.CreateCluster(attr)
   330  	if err != nil {
   331  		return err
   332  	}
   333  	if existing == nil {
   334  		return fmt.Errorf("couldn't create cluster")
   335  	}
   336  	return nil
   337  }
   338  
   339  func getIdAndName(input string) (id, name *string) {
   340  	if strings.HasPrefix(input, "@") {
   341  		h := strings.Trim(input, "@")
   342  		name = &h
   343  	} else {
   344  		id = &input
   345  	}
   346  	return
   347  }
   348  
   349  func (p *Plural) handleCreateProvider(existingProviders []string) (*gqlclient.CreateClusterProvider, error) {
   350  	provider := ""
   351  	var resp struct {
   352  		Name      string
   353  		Namespace string
   354  	}
   355  	if err := survey.Ask(providerSurvey, &resp); err != nil {
   356  		return nil, err
   357  	}
   358  
   359  	prompt := &survey.Select{
   360  		Message: "Select one of the following providers:",
   361  		Options: existingProviders,
   362  	}
   363  	if err := survey.AskOne(prompt, &provider, survey.WithValidator(survey.Required)); err != nil {
   364  		return nil, err
   365  	}
   366  
   367  	cps, err := cd.AskCloudProviderSettings(provider)
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  
   372  	providerAttr := gqlclient.ClusterProviderAttributes{
   373  		Name:          resp.Name,
   374  		Namespace:     &resp.Namespace,
   375  		Cloud:         &provider,
   376  		CloudSettings: cps,
   377  	}
   378  	clusterProv, err := p.ConsoleClient.CreateProvider(providerAttr)
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  	if clusterProv == nil {
   383  		return nil, fmt.Errorf("provider was not created properly")
   384  	}
   385  	return clusterProv, nil
   386  }
   387  
   388  func (p *Plural) handleClusterReinstall(c *cli.Context) error {
   389  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   390  		return err
   391  	}
   392  
   393  	deployToken, err := p.ConsoleClient.GetDeployToken(getIdAndName(c.Args().Get(0)))
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	url := fmt.Sprintf("%s/ext/gql", p.ConsoleClient.Url())
   399  	return p.doInstallOperator(url, deployToken, c.String("values"))
   400  }
   401  
   402  func (p *Plural) handleClusterBootstrap(c *cli.Context) error {
   403  	if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil {
   404  		return err
   405  	}
   406  
   407  	attrs := gqlclient.ClusterAttributes{Name: c.String("name")}
   408  	if c.String("handle") != "" {
   409  		attrs.Handle = lo.ToPtr(c.String("handle"))
   410  	}
   411  
   412  	if c.IsSet("tag") {
   413  		attrs.Tags = lo.Map(c.StringSlice("tag"), func(tag string, index int) *gqlclient.TagAttributes {
   414  			tags := strings.Split(tag, "=")
   415  			if len(tags) == 2 {
   416  				return &gqlclient.TagAttributes{
   417  					Name:  tags[0],
   418  					Value: tags[1],
   419  				}
   420  			}
   421  			return nil
   422  		})
   423  		attrs.Tags = lo.Filter(attrs.Tags, func(t *gqlclient.TagAttributes, ind int) bool { return t != nil })
   424  	}
   425  
   426  	existing, err := p.ConsoleClient.CreateCluster(attrs)
   427  	if err != nil {
   428  		return err
   429  	}
   430  
   431  	if existing.CreateCluster.DeployToken == nil {
   432  		return fmt.Errorf("could not fetch deploy token from cluster")
   433  	}
   434  
   435  	deployToken := *existing.CreateCluster.DeployToken
   436  	url := fmt.Sprintf("%s/ext/gql", p.ConsoleClient.Url())
   437  	utils.Highlight("installing agent on %s with url %s and initial deploy token %s\n", c.String("name"), p.ConsoleClient.Url(), deployToken)
   438  	return p.doInstallOperator(url, deployToken, c.String("values"))
   439  }