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 }