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  }