github.com/pluralsh/plural-cli@v0.9.5/cmd/plural/cd_services.go (about) 1 package plural 2 3 import ( 4 "fmt" 5 "strings" 6 7 gqlclient "github.com/pluralsh/console-client-go" 8 "github.com/pluralsh/plural-cli/pkg/cd/template" 9 "github.com/pluralsh/plural-cli/pkg/console" 10 "github.com/pluralsh/plural-cli/pkg/utils" 11 "github.com/pluralsh/polly/containers" 12 "github.com/samber/lo" 13 "github.com/urfave/cli" 14 "k8s.io/apimachinery/pkg/util/yaml" 15 ) 16 17 func (p *Plural) cdServices() cli.Command { 18 return cli.Command{ 19 Name: "services", 20 Subcommands: p.cdServiceCommands(), 21 Usage: "manage CD services", 22 } 23 } 24 25 func (p *Plural) cdServiceCommands() []cli.Command { 26 return []cli.Command{ 27 { 28 Name: "list", 29 ArgsUsage: "CLUSTER_ID", 30 Action: latestVersion(requireArgs(p.handleListClusterServices, []string{"CLUSTER_ID"})), 31 Usage: "list cluster services", 32 }, 33 { 34 Name: "create", 35 ArgsUsage: "CLUSTER_ID", 36 Flags: []cli.Flag{ 37 cli.StringFlag{Name: "name", Usage: "service name", Required: true}, 38 cli.StringFlag{Name: "namespace", Usage: "service namespace. If not specified the 'default' will be used"}, 39 cli.StringFlag{Name: "version", Usage: "service version. If not specified the '0.0.1' will be used"}, 40 cli.StringFlag{Name: "repo-id", Usage: "repository ID", Required: true}, 41 cli.StringFlag{Name: "git-ref", Usage: "git ref, can be branch, tag or commit sha", Required: true}, 42 cli.StringFlag{Name: "git-folder", Usage: "folder within the source tree where manifests are located", Required: true}, 43 cli.StringFlag{Name: "kustomize-folder", Usage: "folder within the kustomize file is located"}, 44 cli.BoolFlag{Name: "dry-run", Usage: "dry run mode"}, 45 cli.StringSliceFlag{ 46 Name: "conf", 47 Usage: "config name value", 48 }, 49 cli.StringFlag{Name: "config-file", Usage: "path for configuration file"}, 50 }, 51 Action: latestVersion(requireArgs(p.handleCreateClusterService, []string{"CLUSTER_ID"})), 52 Usage: "create cluster service", 53 }, 54 { 55 Name: "update", 56 ArgsUsage: "SERVICE_ID", 57 Action: latestVersion(requireArgs(p.handleUpdateClusterService, []string{"SERVICE_ID"})), 58 Usage: "update cluster service", 59 Flags: []cli.Flag{ 60 cli.StringFlag{Name: "version", Usage: "service version"}, 61 cli.StringFlag{Name: "git-ref", Usage: "git ref, can be branch, tag or commit sha"}, 62 cli.StringFlag{Name: "git-folder", Usage: "folder within the source tree where manifests are located"}, 63 cli.StringFlag{Name: "kustomize-folder", Usage: "folder within the kustomize file is located"}, 64 cli.StringSliceFlag{ 65 Name: "conf", 66 Usage: "config name value", 67 }, 68 cli.BoolFlag{Name: "dry-run", Usage: "dry run mode"}, 69 cli.BoolFlag{Name: "templated", Usage: "set templated flag"}, 70 cli.StringSliceFlag{ 71 Name: "context-id", 72 Usage: "bind service context", 73 }, 74 }, 75 }, 76 { 77 Name: "clone", 78 ArgsUsage: "CLUSTER SERVICE", 79 Action: latestVersion(requireArgs(p.handleCloneClusterService, []string{"CLUSTER", "SERVICE"})), 80 Flags: []cli.Flag{ 81 cli.StringFlag{Name: "name", Usage: "the name for the cloned service", Required: true}, 82 cli.StringFlag{Name: "namespace", Usage: "the namespace for this cloned service", Required: true}, 83 cli.StringSliceFlag{ 84 Name: "conf", 85 Usage: "config name value", 86 }, 87 }, 88 Usage: "deep clone a service onto either the same cluster or another", 89 }, 90 { 91 Name: "describe", 92 ArgsUsage: "SERVICE_ID", 93 Action: latestVersion(requireArgs(p.handleDescribeClusterService, []string{"SERVICE_ID"})), 94 Flags: []cli.Flag{ 95 cli.StringFlag{Name: "o", Usage: "output format"}, 96 }, 97 Usage: "describe cluster service", 98 }, 99 { 100 Name: "template", 101 Action: p.handleTemplateService, 102 Usage: "Dry-runs templating a .liquid or .tpl file with either a full service as params or custom config", 103 Flags: []cli.Flag{ 104 cli.StringFlag{ 105 Name: "service", 106 Usage: "specify the service you want to use as context while templating"}, 107 cli.StringFlag{ 108 Name: "configuration", 109 Usage: "hand-coded configuration for templating (useful if you want to test before creating a service)", 110 }, 111 cli.StringFlag{ 112 Name: "file", 113 Usage: "The .liquid or .tpl file you want to attempt to template.", 114 }, 115 }, 116 }, 117 { 118 Name: "delete", 119 ArgsUsage: "SERVICE_ID", 120 Action: latestVersion(requireArgs(p.handleDeleteClusterService, []string{"SERVICE_ID"})), 121 Usage: "delete cluster service", 122 }, 123 { 124 Name: "kick", 125 ArgsUsage: "SERVICE_ID", 126 Action: latestVersion(requireArgs(p.handleKickClusterService, []string{"SERVICE_ID"})), 127 Usage: "force sync cluster service", 128 }, 129 } 130 } 131 132 func (p *Plural) handleListClusterServices(c *cli.Context) error { 133 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 134 return err 135 } 136 sd, err := p.ConsoleClient.ListClusterServices(getIdAndName(c.Args().Get(0))) 137 if err != nil { 138 return err 139 } 140 if sd == nil { 141 return fmt.Errorf("returned objects list [ListClusterServices] is nil") 142 } 143 headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} 144 return utils.PrintTable(sd, headers, func(sd *gqlclient.ServiceDeploymentEdgeFragment) ([]string, error) { 145 ref := "" 146 folder := "" 147 url := "" 148 if sd.Node.Git != nil { 149 ref = sd.Node.Git.Ref 150 folder = sd.Node.Git.Folder 151 } 152 if sd.Node.Repository != nil { 153 url = sd.Node.Repository.URL 154 } 155 return []string{sd.Node.ID, sd.Node.Name, sd.Node.Namespace, ref, folder, url}, nil 156 }) 157 } 158 159 func (p *Plural) handleCreateClusterService(c *cli.Context) error { 160 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 161 return err 162 } 163 v, err := validateFlag(c, "version", "0.0.1") 164 if err != nil { 165 return err 166 } 167 name := c.String("name") 168 namespace, err := validateFlag(c, "namespace", "default") 169 if err != nil { 170 return err 171 } 172 repoId := c.String("repo-id") 173 gitRef := c.String("git-ref") 174 gitFolder := c.String("git-folder") 175 dryRun := c.Bool("dry-run") 176 attributes := gqlclient.ServiceDeploymentAttributes{ 177 Name: name, 178 Namespace: namespace, 179 Version: &v, 180 RepositoryID: lo.ToPtr(repoId), 181 Git: &gqlclient.GitRefAttributes{ 182 Ref: gitRef, 183 Folder: gitFolder, 184 }, 185 Configuration: []*gqlclient.ConfigAttributes{}, 186 DryRun: lo.ToPtr(dryRun), 187 } 188 189 if c.String("kustomize-folder") != "" { 190 attributes.Kustomize = &gqlclient.KustomizeAttributes{ 191 Path: c.String("kustomize-folder"), 192 } 193 } 194 195 if c.String("config-file") != "" { 196 configFile, err := utils.ReadFile(c.String("config-file")) 197 if err != nil { 198 return err 199 } 200 sdc := ServiceDeploymentAttributesConfiguration{} 201 if err := yaml.Unmarshal([]byte(configFile), &sdc); err != nil { 202 return err 203 } 204 attributes.Configuration = append(attributes.Configuration, sdc.Configuration...) 205 } 206 var confArgs []string 207 if c.IsSet("conf") { 208 confArgs = append(confArgs, c.StringSlice("conf")...) 209 } 210 for _, conf := range confArgs { 211 configurationPair := strings.Split(conf, "=") 212 if len(configurationPair) == 2 { 213 attributes.Configuration = append(attributes.Configuration, &gqlclient.ConfigAttributes{ 214 Name: configurationPair[0], 215 Value: &configurationPair[1], 216 }) 217 } 218 } 219 220 clusterId, clusterName := getIdAndName(c.Args().Get(0)) 221 sd, err := p.ConsoleClient.CreateClusterService(clusterId, clusterName, attributes) 222 if err != nil { 223 return err 224 } 225 if sd == nil { 226 return fmt.Errorf("the returned object is empty, check if all fields are set") 227 } 228 229 headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} 230 return utils.PrintTable([]*gqlclient.ServiceDeploymentExtended{sd}, headers, func(sd *gqlclient.ServiceDeploymentExtended) ([]string, error) { 231 return []string{sd.ID, sd.Name, sd.Namespace, sd.Git.Ref, sd.Git.Folder, sd.Repository.URL}, nil 232 }) 233 } 234 235 func (p *Plural) handleTemplateService(c *cli.Context) error { 236 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 237 return err 238 } 239 240 printResult := func(out []byte) error { 241 fmt.Println() 242 fmt.Println(string(out)) 243 return nil 244 } 245 246 if identifier := c.String("service"); identifier != "" { 247 serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(identifier) 248 if err != nil { 249 return err 250 } 251 252 existing, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) 253 if err != nil { 254 return err 255 } 256 if existing == nil { 257 return fmt.Errorf("Service %s does not exist", identifier) 258 } 259 260 res, err := template.RenderService(c.String("file"), existing) 261 if err != nil { 262 return err 263 } 264 return printResult(res) 265 } 266 267 bindings := map[string]interface{}{} 268 if err := utils.YamlFile(c.String("configuration"), &bindings); err != nil { 269 return err 270 } 271 272 res, err := template.RenderYaml(c.String("file"), bindings) 273 if err != nil { 274 return err 275 } 276 return printResult(res) 277 } 278 279 func (p *Plural) handleCloneClusterService(c *cli.Context) error { 280 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 281 return err 282 } 283 284 cluster, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0))) 285 if err != nil { 286 return err 287 } 288 if cluster == nil { 289 return fmt.Errorf("could not find cluster %s", c.Args().Get(0)) 290 } 291 292 serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(1)) 293 if err != nil { 294 return err 295 } 296 297 attributes := gqlclient.ServiceCloneAttributes{ 298 Name: c.String("name"), 299 Namespace: lo.ToPtr(c.String("namespace")), 300 } 301 302 // TODO: DRY this up with service update 303 var confArgs []string 304 if c.IsSet("conf") { 305 confArgs = append(confArgs, c.StringSlice("conf")...) 306 } 307 308 updateConfigurations := map[string]string{} 309 for _, conf := range confArgs { 310 configurationPair := strings.Split(conf, "=") 311 if len(configurationPair) == 2 { 312 updateConfigurations[configurationPair[0]] = configurationPair[1] 313 } 314 } 315 for key, value := range updateConfigurations { 316 attributes.Configuration = append(attributes.Configuration, &gqlclient.ConfigAttributes{ 317 Name: key, 318 Value: lo.ToPtr(value), 319 }) 320 } 321 322 sd, err := p.ConsoleClient.CloneService(cluster.ID, serviceId, serviceName, clusterName, attributes) 323 if err != nil { 324 return err 325 } 326 327 headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} 328 return utils.PrintTable([]*gqlclient.ServiceDeploymentFragment{sd}, headers, func(sd *gqlclient.ServiceDeploymentFragment) ([]string, error) { 329 return []string{sd.ID, sd.Name, sd.Namespace, sd.Git.Ref, sd.Git.Folder, sd.Repository.URL}, nil 330 }) 331 } 332 333 func (p *Plural) handleUpdateClusterService(c *cli.Context) error { 334 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 335 return err 336 } 337 contextBindings := containers.NewSet[string]() 338 serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0)) 339 if err != nil { 340 return err 341 } 342 343 existing, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) 344 if err != nil { 345 return err 346 } 347 if existing == nil { 348 return fmt.Errorf("existing service deployment is empty") 349 } 350 existingConfigurations := map[string]string{} 351 attributes := gqlclient.ServiceUpdateAttributes{ 352 Version: &existing.Version, 353 Git: &gqlclient.GitRefAttributes{ 354 Ref: existing.Git.Ref, 355 Folder: existing.Git.Folder, 356 }, 357 Configuration: []*gqlclient.ConfigAttributes{}, 358 } 359 for _, context := range existing.Contexts { 360 contextBindings.Add(context.ID) 361 } 362 363 if existing.DryRun != nil { 364 attributes.DryRun = existing.DryRun 365 } 366 if existing.Kustomize != nil { 367 attributes.Kustomize = &gqlclient.KustomizeAttributes{ 368 Path: existing.Kustomize.Path, 369 } 370 } 371 372 for _, conf := range existing.Configuration { 373 existingConfigurations[conf.Name] = conf.Value 374 } 375 376 v := c.String("version") 377 if v != "" { 378 attributes.Version = &v 379 } 380 if c.String("git-ref") != "" { 381 attributes.Git.Ref = c.String("git-ref") 382 } 383 if c.String("git-folder") != "" { 384 attributes.Git.Folder = c.String("git-folder") 385 } 386 var confArgs []string 387 if c.IsSet("conf") { 388 confArgs = append(confArgs, c.StringSlice("conf")...) 389 } 390 var contextArgs []string 391 if c.IsSet("context-id") { 392 contextArgs = append(contextArgs, c.StringSlice("context-id")...) 393 } 394 for _, context := range contextArgs { 395 contextBindings.Add(context) 396 } 397 if contextBindings.Len() > 0 { 398 attributes.ContextBindings = make([]*gqlclient.ContextBindingAttributes, 0) 399 for _, context := range contextBindings.List() { 400 attributes.ContextBindings = append(attributes.ContextBindings, &gqlclient.ContextBindingAttributes{ 401 ContextID: context, 402 }) 403 } 404 } 405 406 updateConfigurations := map[string]string{} 407 for _, conf := range confArgs { 408 configurationPair := strings.Split(conf, "=") 409 if len(configurationPair) == 2 { 410 updateConfigurations[configurationPair[0]] = configurationPair[1] 411 } 412 } 413 for k, v := range updateConfigurations { 414 existingConfigurations[k] = v 415 } 416 for key, value := range existingConfigurations { 417 attributes.Configuration = append(attributes.Configuration, &gqlclient.ConfigAttributes{ 418 Name: key, 419 Value: lo.ToPtr(value), 420 }) 421 } 422 if c.String("kustomize-folder") != "" { 423 attributes.Kustomize = &gqlclient.KustomizeAttributes{ 424 Path: c.String("kustomize-folder"), 425 } 426 } 427 if c.IsSet("dry-run") { 428 dryRun := c.Bool("dry-run") 429 attributes.DryRun = &dryRun 430 } 431 if c.IsSet("templated") { 432 templated := c.Bool("templated") 433 attributes.Templated = &templated 434 } 435 436 sd, err := p.ConsoleClient.UpdateClusterService(serviceId, serviceName, clusterName, attributes) 437 if err != nil { 438 return err 439 } 440 if sd == nil { 441 return fmt.Errorf("returned object is nil") 442 } 443 444 headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} 445 return utils.PrintTable([]*gqlclient.ServiceDeploymentExtended{sd}, headers, func(sd *gqlclient.ServiceDeploymentExtended) ([]string, error) { 446 return []string{sd.ID, sd.Name, sd.Namespace, sd.Git.Ref, sd.Git.Folder, sd.Repository.URL}, nil 447 }) 448 } 449 450 func (p *Plural) handleDescribeClusterService(c *cli.Context) error { 451 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 452 return err 453 } 454 455 serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0)) 456 if err != nil { 457 return err 458 } 459 existing, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) 460 if err != nil { 461 return err 462 } 463 if existing == nil { 464 return fmt.Errorf("existing service deployment is empty") 465 } 466 output := c.String("o") 467 if output == "json" { 468 utils.NewJsonPrinter(existing).PrettyPrint() 469 return nil 470 } else if output == "yaml" { 471 utils.NewYAMLPrinter(existing).PrettyPrint() 472 return nil 473 } 474 desc, err := console.DescribeService(existing) 475 if err != nil { 476 return err 477 } 478 fmt.Print(desc) 479 480 return nil 481 } 482 483 func (p *Plural) handleDeleteClusterService(c *cli.Context) error { 484 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 485 return err 486 } 487 serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0)) 488 if err != nil { 489 return err 490 } 491 492 svc, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) 493 if err != nil { 494 return err 495 } 496 if svc == nil { 497 return fmt.Errorf("Could not find service for %s", c.Args().Get(0)) 498 } 499 500 deleted, err := p.ConsoleClient.DeleteClusterService(svc.ID) 501 if err != nil { 502 return fmt.Errorf("could not delete service: %w", err) 503 } 504 505 utils.Success("Service %s has been deleted successfully\n", deleted.DeleteServiceDeployment.Name) 506 return nil 507 } 508 509 func (p *Plural) handleKickClusterService(c *cli.Context) error { 510 if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { 511 return err 512 } 513 serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0)) 514 if err != nil { 515 return err 516 } 517 svc, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) 518 if err != nil { 519 return err 520 } 521 if svc == nil { 522 return fmt.Errorf("Could not find service for %s", c.Args().Get(0)) 523 } 524 kick, err := p.ConsoleClient.KickClusterService(serviceId, serviceName, clusterName) 525 if err != nil { 526 return err 527 } 528 utils.Success("Service %s has been sync successfully\n", kick.Name) 529 return nil 530 } 531 532 type ServiceDeploymentAttributesConfiguration struct { 533 Configuration []*gqlclient.ConfigAttributes 534 } 535 536 func getServiceIdClusterNameServiceName(input string) (serviceId, clusterName, serviceName *string, err error) { 537 if strings.HasPrefix(input, "@") { 538 i := strings.Trim(input, "@") 539 split := strings.Split(i, "/") 540 if len(split) != 2 { 541 err = fmt.Errorf("expected format @clusterName/serviceName") 542 return 543 } 544 clusterName = &split[0] 545 serviceName = &split[1] 546 } else { 547 serviceId = &input 548 } 549 return 550 } 551 552 func validateFlag(ctx *cli.Context, name string, defaultVal string) (string, error) { 553 res := ctx.String(name) 554 if res == "" { 555 if defaultVal == "" { 556 return "", fmt.Errorf("expected --%s flag", name) 557 } 558 res = defaultVal 559 } 560 561 return res, nil 562 }