From 30f7c3e0f9b39e6c8f88c35b5f9876c212b6ceea Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Sun, 12 Nov 2023 23:54:40 +0100 Subject: [PATCH] [WIP]create deployments commands (#455) * create deployments commands * add repositories commands * update client auth * add deployments repositories create command * add deployments repositories list command * refactor * bump go client * fix list deployments * use go client model * create and update service deployment * Small style tweaks and show repository health in `repositories list` (#458) * Small style tweaks and show repository health in `repositories list` Pull status is definitely nice to be able to see here. * show pull errors as well * describe service * install agent helm method * create agent namespace * enable CAPI feature flag for e2e * add services delete command * Extend clusters list display (#460) Shows the provider, and shows repo by url instead of id in some places where useful * describe cluster + code refacor * add flag validation * support handle for plural deployments services list/describe * add required flags * add handle for create CD service * add wait flag for agent installation * update services by handle * add console client mocks * improve describe cluster and service * read kubeconfig from env var if present * respect KUBECONFIG env var in plural kube client too * force update helm repos on install to prevent stale chart installations * use correct release name * don't swallow helm errors * extend update cluster for kubeconfig * add plural cd providers list command * create/delete provider credentials * pipeline create command * fix some repositories commands * add deployments clusters get-credentials command (#467) * Add a persistent config file for console logins on cli * Implement `plural cd clusters delete` (#468) When the console-client-go pr is merged, we can implement the `--soft` flag too. * fix pointer bug on config updates * add create cluster command * create azure cluster * fix after schema changes * bump console client * bump console client * update arg name * refactor cluster commands * refactor provider command * remove provider id param * don't wait in cd install command * add clusters bootstrap command * add operator uninstall, cluster tags on bootstrap * more tweaks to agent installer * some more agent install tweaks * update gcp provider * update provider create * fix profile list cmd * filter out existing providers * remove gcp credenitials b64 encode logic * BYOK installer for plural console * wrap up console installer * fix linter * refactor cd structure * fix unit tests * fix cd install command * add kustomize support * init eject command * fix token name for cd install * make cd clusters kubeconfig more robust * add kas_dns to server context * add eject command and fix gcp capi provider bootstrapping * modify control plane installer * add service clone support --------- Co-authored-by: Marcin Maciaszczyk Co-authored-by: michaeljguarino Co-authored-by: Sebastian Florek --- cmd/plural/cd.go | 154 +++++++++ cmd/plural/cd_clusters.go | 407 +++++++++++++++++++++++ cmd/plural/cd_credentials.go | 79 +++++ cmd/plural/cd_pipelines.go | 65 ++++ cmd/plural/cd_providers.go | 148 +++++++++ cmd/plural/cd_repositories.go | 136 ++++++++ cmd/plural/cd_services.go | 418 +++++++++++++++++++++++ cmd/plural/cd_test.go | 177 ++++++++++ cmd/plural/config_test.go | 4 +- cmd/plural/plural.go | 49 ++- go.mod | 7 +- go.sum | 16 +- hack/e2e/setup-plural.sh | 2 +- hack/gen-client-mocks.sh | 3 +- hack/pipeline.yaml | 15 + pkg/api/client.go | 1 + pkg/api/installations.go | 8 + pkg/bootstrap/cilium.go | 76 +---- pkg/bootstrap/common.go | 12 +- pkg/bundle/oidc.go | 23 ++ pkg/cd/clusters_create.go | 113 +++++++ pkg/cd/clusters_get_credentials.go | 52 +++ pkg/cd/control_plane_install.go | 140 ++++++++ pkg/cd/eject.go | 9 + pkg/cd/providers_create.go | 148 +++++++++ pkg/config/config.go | 6 +- pkg/console/agent.go | 73 ++++ pkg/console/clusters.go | 60 ++++ pkg/console/config.go | 61 ++++ pkg/console/console.go | 59 ++++ pkg/console/describe.go | 263 +++++++++++++++ pkg/console/pipelines.go | 94 ++++++ pkg/console/providers.go | 45 +++ pkg/console/repositories.go | 39 +++ pkg/console/services.go | 138 ++++++++ pkg/helm/helm.go | 122 +++++++ pkg/kubernetes/config/kubeconfig.go | 47 +++ pkg/kubernetes/kube.go | 14 + pkg/scaffold/template/lua_test.go | 2 + pkg/server/setup.go | 1 + pkg/test/mocks/Client.go | 26 +- pkg/test/mocks/ConsoleClient.go | 497 ++++++++++++++++++++++++++++ pkg/test/mocks/Kube.go | 16 +- pkg/utils/print.go | 31 ++ 44 files changed, 3754 insertions(+), 102 deletions(-) create mode 100644 cmd/plural/cd.go create mode 100644 cmd/plural/cd_clusters.go create mode 100644 cmd/plural/cd_credentials.go create mode 100644 cmd/plural/cd_pipelines.go create mode 100644 cmd/plural/cd_providers.go create mode 100644 cmd/plural/cd_repositories.go create mode 100644 cmd/plural/cd_services.go create mode 100644 cmd/plural/cd_test.go create mode 100644 hack/pipeline.yaml create mode 100644 pkg/cd/clusters_create.go create mode 100644 pkg/cd/clusters_get_credentials.go create mode 100644 pkg/cd/control_plane_install.go create mode 100644 pkg/cd/eject.go create mode 100644 pkg/cd/providers_create.go create mode 100644 pkg/console/agent.go create mode 100644 pkg/console/clusters.go create mode 100644 pkg/console/config.go create mode 100644 pkg/console/console.go create mode 100644 pkg/console/describe.go create mode 100644 pkg/console/pipelines.go create mode 100644 pkg/console/providers.go create mode 100644 pkg/console/repositories.go create mode 100644 pkg/console/services.go create mode 100644 pkg/kubernetes/config/kubeconfig.go create mode 100644 pkg/test/mocks/ConsoleClient.go diff --git a/cmd/plural/cd.go b/cmd/plural/cd.go new file mode 100644 index 00000000..5021d5c5 --- /dev/null +++ b/cmd/plural/cd.go @@ -0,0 +1,154 @@ +package plural + +import ( + "fmt" + "os" + + "github.com/urfave/cli" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/pluralsh/plural/pkg/cd" + "github.com/pluralsh/plural/pkg/config" + "github.com/pluralsh/plural/pkg/console" + "github.com/pluralsh/plural/pkg/utils" +) + +func init() { + consoleToken = "" + consoleURL = "" +} + +const ( + operatorNamespace = "plrl-deploy-operator" +) + +var consoleToken string +var consoleURL string + +func (p *Plural) cdCommands() []cli.Command { + return []cli.Command{ + p.cdProviders(), + p.cdCredentials(), + p.cdClusters(), + p.cdServices(), + p.cdRepositories(), + p.cdPipelines(), + { + Name: "install", + Action: p.handleInstallDeploymentsOperator, + Usage: "install deployments operator", + Flags: []cli.Flag{ + cli.StringFlag{Name: "url", Usage: "console url", Required: true}, + cli.StringFlag{Name: "token", Usage: "deployment token", Required: true}, + }, + }, + { + Name: "control-plane", + Action: p.handleInstallControlPlane, + Usage: "sets up the plural console in an existing k8s cluster", + }, + { + Name: "uninstall", + Action: p.handleUninstallOperator, + Usage: "uninstalls the deployment operator from the current cluster", + }, + { + Name: "login", + Action: p.handleCdLogin, + Usage: "logs into your plural console", + Flags: []cli.Flag{ + cli.StringFlag{Name: "url", Usage: "console url", Required: true}, + cli.StringFlag{Name: "token", Usage: "console access token"}, + }, + }, + { + Name: "eject", + Action: p.handleEject, + Usage: "ejects cluster scaffolds", + ArgsUsage: "", + // TODO: enable once logic is finished + Hidden: true, + }, + } +} + +func (p *Plural) handleInstallDeploymentsOperator(c *cli.Context) error { + return p.doInstallOperator(c.String("url"), c.String("token")) +} + +func (p *Plural) handleUninstallOperator(_ *cli.Context) error { + err := p.InitKube() + if err != nil { + return err + } + return console.UninstallAgent(operatorNamespace) +} + +func (p *Plural) doInstallOperator(url, token string) error { + err := p.InitKube() + if err != nil { + return err + } + err = p.Kube.CreateNamespace(operatorNamespace) + if err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + err = console.InstallAgent(url, token, operatorNamespace) + if err == nil { + utils.Success("deployment operator installed successfully\n") + } + return err +} + +func (p *Plural) handleCdLogin(c *cli.Context) (err error) { + url := c.String("url") + token := c.String("token") + if token == "" { + token, err = utils.ReadPwd("Enter your console access token") + if err != nil { + return + } + } + conf := console.Config{Url: url, Token: token} + return conf.Save() +} + +func (p *Plural) handleInstallControlPlane(_ *cli.Context) error { + conf := config.Read() + vals, err := cd.CreateControlPlane(conf) + if err != nil { + return err + } + + fmt.Print("\n\n") + utils.Highlight("===> writing values.secret.yaml, you should keep this in a secure location for future helm upgrades\n\n") + if err := os.WriteFile("values.secret.yaml", []byte(vals), 0644); err != nil { + return err + } + + fmt.Println("After confirming everything looks correct in values.secret.yaml, run the following command to install:") + utils.Highlight("helm upgrade --install --create-namespace -f values.secret.yaml --repo https://pluralsh.github.io/console console console -n plrl-console") + return nil +} + +func (p *Plural) handleEject(c *cli.Context) (err error) { + if !c.Args().Present() { + return fmt.Errorf("clusterid cannot be empty") + } + + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + clusterId := c.Args().First() + cluster, err := p.ConsoleClient.GetCluster(&clusterId, nil) + if err != nil { + return err + } + + if cluster == nil { + return fmt.Errorf("could not find cluster with given id") + } + + return cd.Eject(cluster) +} diff --git a/cmd/plural/cd_clusters.go b/cmd/plural/cd_clusters.go new file mode 100644 index 00000000..114d3e69 --- /dev/null +++ b/cmd/plural/cd_clusters.go @@ -0,0 +1,407 @@ +package plural + +import ( + "fmt" + "strings" + + "github.com/AlecAivazis/survey/v2" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/cd" + "github.com/pluralsh/plural/pkg/console" + "github.com/pluralsh/plural/pkg/kubernetes/config" + "github.com/pluralsh/plural/pkg/utils" + "github.com/pluralsh/polly/containers" + "github.com/samber/lo" + "github.com/urfave/cli" +) + +var providerSurvey = []*survey.Question{ + { + Name: "name", + Prompt: &survey.Input{Message: "Enter the name of your provider:"}, + }, + { + Name: "namespace", + Prompt: &survey.Input{Message: "Enter the namespace of your provider:"}, + }, +} + +func (p *Plural) cdClusters() cli.Command { + return cli.Command{ + Name: "clusters", + Subcommands: p.cdClusterCommands(), + Usage: "manage CD clusters", + } +} + +func (p *Plural) cdClusterCommands() []cli.Command { + return []cli.Command{ + { + Name: "list", + Action: latestVersion(p.handleListClusters), + Usage: "list clusters", + }, + { + Name: "describe", + Action: latestVersion(requireArgs(p.handleDescribeCluster, []string{"CLUSTER_ID"})), + Usage: "describe cluster", + ArgsUsage: "CLUSTER_ID", + Flags: []cli.Flag{ + cli.StringFlag{Name: "o", Usage: "output format"}, + }, + }, + { + Name: "update", + Action: latestVersion(requireArgs(p.handleUpdateCluster, []string{"CLUSTER_ID"})), + Usage: "update cluster", + ArgsUsage: "CLUSTER_ID", + Flags: []cli.Flag{ + cli.StringFlag{Name: "handle", Usage: "unique human readable name used to identify this cluster"}, + cli.StringFlag{Name: "kubeconf-path", Usage: "path to kubeconfig"}, + cli.StringFlag{Name: "kubeconf-context", Usage: "the kubeconfig context you want to use. If not specified, the current one will be used"}, + }, + }, + { + Name: "delete", + Action: latestVersion(requireArgs(p.handleDeleteCluster, []string{"CLUSTER_ID"})), + Usage: "deregisters a cluster in plural cd, and drains all services (unless --soft is specified)", + ArgsUsage: "CLUSTER_ID", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "soft", Usage: "deletes a cluster in our system but doesn't drain resources, leaving them untouched"}, + }, + }, + { + Name: "get-credentials", + Aliases: []string{"kubeconfig"}, + Action: latestVersion(requireArgs(p.handleGetClusterCredentials, []string{"CLUSTER_ID"})), + Usage: "updates kubeconfig file with appropriate credentials to point to specified cluster", + ArgsUsage: "CLUSTER_ID", + }, + { + Name: "create", + Action: latestVersion(requireArgs(p.handleCreateCluster, []string{"CLUSTER_NAME"})), + Usage: "create cluster", + ArgsUsage: "CLUSTER_NAME", + Flags: []cli.Flag{ + cli.StringFlag{Name: "handle", Usage: "unique human readable name used to identify this cluster"}, + cli.StringFlag{Name: "version", Usage: "kubernetes cluster version", Required: true}, + }, + }, + { + Name: "bootstrap", + Action: latestVersion(p.handleClusterBootstrap), + Usage: "creates a new BYOK cluster and installs the agent onto it using the current kubeconfig", + Flags: []cli.Flag{ + cli.StringFlag{Name: "name", Usage: "The name you'll give the cluster", Required: true}, + cli.StringFlag{Name: "handle", Usage: "optional handle for the cluster"}, + cli.StringSliceFlag{ + Name: "tag", + Usage: "a cluster tag to add, useful for targeting with global services", + }, + }, + }, + } +} + +func (p *Plural) handleListClusters(_ *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + clusters, err := p.ConsoleClient.ListClusters() + if err != nil { + return err + } + if clusters == nil { + return fmt.Errorf("returned objects list [ListClusters] is nil") + } + headers := []string{"Id", "Name", "Handle", "Version", "Provider"} + return utils.PrintTable(clusters.Clusters.Edges, headers, func(cl *gqlclient.ClusterEdgeFragment) ([]string, error) { + provider := "" + if cl.Node.Provider != nil { + provider = cl.Node.Provider.Name + } + handle := "" + if cl.Node.Handle != nil { + handle = *cl.Node.Handle + } + version := "" + if cl.Node.Version != nil { + version = *cl.Node.Version + } + return []string{cl.Node.ID, cl.Node.Name, handle, version, provider}, nil + }) +} + +func (p *Plural) handleDescribeCluster(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + existing, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0))) + if err != nil { + return err + } + if existing == nil { + return fmt.Errorf("existing cluster is empty") + } + output := c.String("o") + if output == "json" { + utils.NewJsonPrinter(existing).PrettyPrint() + } else if output == "yaml" { + utils.NewYAMLPrinter(existing).PrettyPrint() + } + desc, err := console.DescribeCluster(existing) + if err != nil { + return err + } + fmt.Print(desc) + return nil +} + +func (p *Plural) handleUpdateCluster(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + existing, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0))) + if err != nil { + return err + } + if existing == nil { + return fmt.Errorf("this cluster does not exist") + } + updateAttr := gqlclient.ClusterUpdateAttributes{ + Version: existing.Version, + Handle: existing.Handle, + } + newHandle := c.String("handle") + if newHandle != "" { + updateAttr.Handle = &newHandle + } + kubeconfigPath := c.String("kubeconf-path") + if kubeconfigPath != "" { + kubeconfig, err := config.GetKubeconfig(kubeconfigPath, c.String("kubeconf-context")) + if err != nil { + return err + } + + updateAttr.Kubeconfig = &gqlclient.KubeconfigAttributes{ + Raw: &kubeconfig, + } + } + + result, err := p.ConsoleClient.UpdateCluster(existing.ID, updateAttr) + if err != nil { + return err + } + headers := []string{"Id", "Name", "Handle", "Version", "Provider"} + return utils.PrintTable([]gqlclient.ClusterFragment{*result.UpdateCluster}, headers, func(cl gqlclient.ClusterFragment) ([]string, error) { + provider := "" + if cl.Provider != nil { + provider = cl.Provider.Name + } + handle := "" + if cl.Handle != nil { + handle = *cl.Handle + } + return []string{cl.ID, cl.Name, handle, *cl.Version, provider}, nil + }) +} + +func (p *Plural) handleDeleteCluster(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + existing, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0))) + if err != nil { + return err + } + if existing == nil { + return fmt.Errorf("this cluster does not exist") + } + + return p.ConsoleClient.DeleteCluster(existing.ID) +} +func (p *Plural) handleGetClusterCredentials(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + cluster, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0))) + if err != nil { + return err + } + if cluster == nil { + return fmt.Errorf("cluster is nil") + } + + return cd.SaveClusterKubeconfig(cluster, p.ConsoleClient.Token()) +} + +func (p *Plural) handleCreateCluster(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + name := c.Args().Get(0) + attr := gqlclient.ClusterAttributes{ + Name: name, + } + if c.String("handle") != "" { + attr.Handle = lo.ToPtr(c.String("handle")) + } + if c.String("version") != "" { + attr.Version = lo.ToPtr(c.String("version")) + } + + providerList, err := p.ConsoleClient.ListProviders() + if err != nil { + return err + } + providerNames := []string{} + providerMap := map[string]string{} + cloudProviders := []string{} + for _, prov := range providerList.ClusterProviders.Edges { + providerNames = append(providerNames, prov.Node.Name) + providerMap[prov.Node.Name] = prov.Node.ID + cloudProviders = append(cloudProviders, prov.Node.Cloud) + } + + existingProv := containers.ToSet[string](cloudProviders) + availableProv := containers.ToSet[string](availableProviders) + toCreate := availableProv.Difference(existingProv) + createNewProvider := "Create New Provider" + + if toCreate.Len() != 0 { + providerNames = append(providerNames, createNewProvider) + } + + prompt := &survey.Select{ + Message: "Select one of the following providers:", + Options: providerNames, + } + provider := "" + if err := survey.AskOne(prompt, &provider, survey.WithValidator(survey.Required)); err != nil { + return err + } + if provider != createNewProvider { + utils.Success("Using provider %s\n", provider) + id := providerMap[provider] + attr.ProviderID = &id + } else { + + clusterProv, err := p.handleCreateProvider(toCreate.List()) + if err != nil { + return err + } + if clusterProv == nil { + utils.Success("All supported providers are created\n") + return nil + } + utils.Success("Provider %s created successfully\n", clusterProv.CreateClusterProvider.Name) + attr.ProviderID = &clusterProv.CreateClusterProvider.ID + provider = clusterProv.CreateClusterProvider.Cloud + } + + ca, err := cd.AskCloudSettings(provider) + if err != nil { + return err + } + attr.CloudSettings = ca + + existing, err := p.ConsoleClient.CreateCluster(attr) + if err != nil { + return err + } + if existing == nil { + return fmt.Errorf("couldn't create cluster") + } + return nil +} + +func getIdAndName(input string) (id, name *string) { + if strings.HasPrefix(input, "@") { + h := strings.Trim(input, "@") + name = &h + } else { + id = &input + } + return +} + +func (p *Plural) handleCreateProvider(existingProviders []string) (*gqlclient.CreateClusterProvider, error) { + provider := "" + var resp struct { + Name string + Namespace string + } + if err := survey.Ask(providerSurvey, &resp); err != nil { + return nil, err + } + + prompt := &survey.Select{ + Message: "Select one of the following providers:", + Options: existingProviders, + } + if err := survey.AskOne(prompt, &provider, survey.WithValidator(survey.Required)); err != nil { + return nil, err + } + + cps, err := cd.AskCloudProviderSettings(provider) + if err != nil { + return nil, err + } + + providerAttr := gqlclient.ClusterProviderAttributes{ + Name: resp.Name, + Namespace: &resp.Namespace, + Cloud: &provider, + CloudSettings: cps, + } + clusterProv, err := p.ConsoleClient.CreateProvider(providerAttr) + if err != nil { + return nil, err + } + if clusterProv == nil { + return nil, fmt.Errorf("provider was not created properly") + } + return clusterProv, nil +} + +func (p *Plural) handleClusterBootstrap(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + attrs := gqlclient.ClusterAttributes{Name: c.String("name")} + if c.String("handle") != "" { + attrs.Handle = lo.ToPtr(c.String("handle")) + } + + if c.IsSet("tag") { + attrs.Tags = lo.Map(c.StringSlice("tag"), func(tag string, index int) *gqlclient.TagAttributes { + tags := strings.Split(tag, "=") + if len(tags) == 2 { + return &gqlclient.TagAttributes{ + Name: tags[0], + Value: tags[1], + } + } + return nil + }) + attrs.Tags = lo.Filter(attrs.Tags, func(t *gqlclient.TagAttributes, ind int) bool { return t != nil }) + } + + existing, err := p.ConsoleClient.CreateCluster(attrs) + if err != nil { + return err + } + + if existing.CreateCluster.DeployToken == nil { + return fmt.Errorf("could not fetch deploy token from cluster") + } + + deployToken := *existing.CreateCluster.DeployToken + url := fmt.Sprintf("%s/ext/gql", p.ConsoleClient.Url()) + utils.Highlight("instaling agent on %s with url %s and initial deploy token %s\n", c.String("name"), p.ConsoleClient.Url(), deployToken) + return p.doInstallOperator(url, deployToken) +} diff --git a/cmd/plural/cd_credentials.go b/cmd/plural/cd_credentials.go new file mode 100644 index 00000000..41c4f15a --- /dev/null +++ b/cmd/plural/cd_credentials.go @@ -0,0 +1,79 @@ +package plural + +import ( + "fmt" + + "github.com/pkg/errors" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/utils" + "github.com/urfave/cli" +) + +func (p *Plural) cdCredentials() cli.Command { + return cli.Command{ + Name: "credentials", + Subcommands: p.cdCredentialsCommands(), + Usage: "manage Provider credentials", + } +} + +func (p *Plural) cdCredentialsCommands() []cli.Command { + return []cli.Command{ + { + Name: "create", + ArgsUsage: "PROVIDER_NAME", + Action: latestVersion(requireArgs(p.handleCreateProviderCredentials, []string{"PROVIDER_NAME"})), + Usage: "create provider credentials", + }, + { + Name: "delete", + ArgsUsage: "ID", + Action: latestVersion(requireArgs(p.handleDeleteProviderCredentials, []string{"ID"})), + Usage: "delete provider credentials", + }, + } +} + +func (p *Plural) handleDeleteProviderCredentials(c *cli.Context) error { + id := c.Args().Get(0) + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + if _, err := p.ConsoleClient.DeleteProviderCredentials(id); err != nil { + return err + } + utils.Success("Provider credential %s has been deleted successfully", id) + return nil +} + +func (p *Plural) handleCreateProviderCredentials(c *cli.Context) error { + providerName := c.Args().Get(0) + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + attr, err := p.credentialsPreflights() + if err != nil { + return err + } + + resp, err := p.ConsoleClient.CreateProviderCredentials(providerName, *attr) + if err != nil { + errList := errors.New("CreateProviderCredentials") + errList = errors.Wrap(errList, err.Error()) + if *attr.Kind == kindSecret { + if err := p.SecretDelete(*attr.Namespace, attr.Name); err != nil { + errList = errors.Wrap(errList, err.Error()) + } + } + return errList + } + if resp == nil { + return fmt.Errorf("the response from CreateProviderCredentials is empty") + } + + headers := []string{"Id", "Name", "Namespace"} + return utils.PrintTable([]*gqlclient.ProviderCredentialFragment{resp.CreateProviderCredential}, headers, func(sd *gqlclient.ProviderCredentialFragment) ([]string, error) { + return []string{sd.ID, sd.Name, sd.Namespace}, nil + }) +} diff --git a/cmd/plural/cd_pipelines.go b/cmd/plural/cd_pipelines.go new file mode 100644 index 00000000..618a769f --- /dev/null +++ b/cmd/plural/cd_pipelines.go @@ -0,0 +1,65 @@ +package plural + +import ( + "io" + "os" + + "github.com/pluralsh/plural/pkg/console" + "github.com/pluralsh/plural/pkg/utils" + "github.com/urfave/cli" +) + +func (p *Plural) cdPipelines() cli.Command { + return cli.Command{ + Name: "pipelines", + Subcommands: p.pipelineCommands(), + Usage: "manage CD pipelines", + } +} + +func (p *Plural) pipelineCommands() []cli.Command { + return []cli.Command{ + { + Name: "create", + Action: latestVersion(requireArgs(p.handleCreatePipeline, []string{})), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file", + Usage: "the file this pipeline is defined in, use - for stdin", + }, + }, + }, + } +} + +func (p *Plural) handleCreatePipeline(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + var bytes []byte + var err error + file := c.String("file") + if file == "-" { + bytes, err = io.ReadAll(os.Stdin) + } else { + bytes, err = os.ReadFile(file) + } + + if err != nil { + return err + } + + name, attrs, err := console.ConstructPipelineInput(bytes) + if err != nil { + return err + } + + pipe, err := p.ConsoleClient.SavePipeline(name, *attrs) + if err != nil { + return err + } + + utils.Success("Pipeline %s created successfully", pipe.Name) + return nil +} diff --git a/cmd/plural/cd_providers.go b/cmd/plural/cd_providers.go new file mode 100644 index 00000000..2dea55fa --- /dev/null +++ b/cmd/plural/cd_providers.go @@ -0,0 +1,148 @@ +package plural + +import ( + "fmt" + "strconv" + "strings" + + "github.com/AlecAivazis/survey/v2" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/api" + "github.com/pluralsh/plural/pkg/utils" + "github.com/urfave/cli" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const kindSecret = "Secret" + +func (p *Plural) cdProviders() cli.Command { + return cli.Command{ + Name: "providers", + Subcommands: p.cdProvidersCommands(), + Usage: "manage CD providers", + } +} + +func (p *Plural) cdProvidersCommands() []cli.Command { + return []cli.Command{ + { + Name: "list", + Action: latestVersion(p.handleListProviders), + Usage: "list providers", + }, + } +} + +func (p *Plural) handleListProviders(_ *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + providers, err := p.ConsoleClient.ListProviders() + if err != nil { + return err + } + if providers == nil { + return fmt.Errorf("returned objects list [ListProviders] is nil") + } + + headers := []string{"ID", "Name", "Cloud", "Editable", "Repo Url"} + return utils.PrintTable(providers.ClusterProviders.Edges, headers, func(r *gqlclient.ClusterProviderEdgeFragment) ([]string, error) { + editable := "" + if r.Node.Editable != nil { + editable = strconv.FormatBool(*r.Node.Editable) + } + repoUrl := "" + if r.Node.Repository != nil { + repoUrl = r.Node.Repository.URL + } + return []string{r.Node.ID, r.Node.Name, r.Node.Cloud, editable, repoUrl}, nil + }) +} + +var availableProviders = []string{api.ProviderGCP, api.ProviderAzure, api.ProviderAWS} + +func (p *Plural) credentialsPreflights() (*gqlclient.ProviderCredentialAttributes, error) { + provider := "" + prompt := &survey.Select{ + Message: "Select one of the following providers:", + Options: availableProviders, + } + if err := survey.AskOne(prompt, &provider, survey.WithValidator(survey.Required)); err != nil { + return nil, err + } + utils.Success("Using provider %s\n", provider) + if provider == api.ProviderGCP { + kind := kindSecret + name, namespace, err := p.createSecret() + if err != nil { + return nil, err + } + return &gqlclient.ProviderCredentialAttributes{ + Namespace: &namespace, + Name: name, + Kind: &kind, + }, nil + } + + return nil, fmt.Errorf("unsupported provider") +} + +func (p *Plural) createSecret() (name, namespace string, err error) { + err = p.InitKube() + if err != nil { + return "", "", err + } + secretSurvey := []*survey.Question{ + { + Name: "name", + Prompt: &survey.Input{Message: "Enter the name of the secret: "}, + Validate: survey.Required, + }, + { + Name: "namespace", + Prompt: &survey.Input{Message: "Enter the secret namespace: "}, + Validate: survey.Required, + }, + { + Name: "data", + Prompt: &survey.Input{Message: "Enter the secret data pairs name=value, for example: user=admin password=abc : "}, + Validate: survey.Required, + }, + } + var resp struct { + Name string + Namespace string + Data string + } + err = survey.Ask(secretSurvey, &resp) + if err != nil { + return + } + data := getSecretDataPairs(resp.Data) + + providerSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: resp.Name, + Namespace: resp.Namespace, + }, + Data: data, + } + if _, err = p.SecretCreate(resp.Namespace, providerSecret); err != nil { + return + } + name = resp.Name + namespace = resp.Namespace + return +} + +func getSecretDataPairs(in string) map[string][]byte { + res := map[string][]byte{} + for _, conf := range strings.Split(in, " ") { + configurationPair := strings.Split(conf, "=") + if len(configurationPair) == 2 { + res[configurationPair[0]] = []byte(configurationPair[1]) + } + } + return res +} diff --git a/cmd/plural/cd_repositories.go b/cmd/plural/cd_repositories.go new file mode 100644 index 00000000..aae74ee6 --- /dev/null +++ b/cmd/plural/cd_repositories.go @@ -0,0 +1,136 @@ +package plural + +import ( + "fmt" + + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/utils" + "github.com/samber/lo" + "github.com/urfave/cli" +) + +func (p *Plural) cdRepositories() cli.Command { + return cli.Command{ + Name: "repositories", + Subcommands: p.cdRepositoriesCommands(), + Usage: "manage CD repositories", + } +} + +func (p *Plural) cdRepositoriesCommands() []cli.Command { + return []cli.Command{ + { + Name: "list", + Action: latestVersion(p.handleListCDRepositories), + Usage: "list repositories", + }, + { + Name: "create", + Action: latestVersion(p.handleCreateCDRepository), + Flags: []cli.Flag{ + cli.StringFlag{Name: "url", Usage: "git repo url", Required: true}, + cli.StringFlag{Name: "private-key", Usage: "git repo private key"}, + cli.StringFlag{Name: "passphrase", Usage: "git repo passphrase"}, + cli.StringFlag{Name: "username", Usage: "git repo username"}, + cli.StringFlag{Name: "password", Usage: "git repo password"}, + }, + Usage: "create repository", + }, + { + Name: "update", + ArgsUsage: "REPO_ID", + Action: latestVersion(requireArgs(p.handleUpdateCDRepository, []string{"REPO_ID"})), + Flags: []cli.Flag{ + cli.StringFlag{Name: "url", Usage: "git repo url", Required: true}, + cli.StringFlag{Name: "private-key", Usage: "git repo private key"}, + cli.StringFlag{Name: "passphrase", Usage: "git repo passphrase"}, + cli.StringFlag{Name: "username", Usage: "git repo username"}, + cli.StringFlag{Name: "password", Usage: "git repo password"}, + }, + Usage: "update repository", + }, + } +} + +func (p *Plural) handleListCDRepositories(_ *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + repos, err := p.ConsoleClient.ListRepositories() + if err != nil { + return err + } + if repos == nil { + return fmt.Errorf("returned objects list [ListRepositories] is nil") + } + headers := []string{"ID", "URL", "Status", "Error"} + return utils.PrintTable(repos.GitRepositories.Edges, headers, func(r *gqlclient.GitRepositoryEdgeFragment) ([]string, error) { + health := "UNKNOWN" + if r.Node.Health != nil { + health = string(*r.Node.Health) + } + return []string{r.Node.ID, r.Node.URL, health, lo.FromPtr(r.Node.Error)}, nil + }) + +} + +func (p *Plural) handleCreateCDRepository(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + url := c.String("url") + repo, err := p.ConsoleClient.CreateRepository(url, getFlag(c.String("privateKey")), + getFlag(c.String("passphrase")), getFlag(c.String("username")), getFlag(c.String("password"))) + if err != nil { + return err + } + + headers := []string{"ID", "URL"} + return utils.PrintTable([]gqlclient.GitRepositoryFragment{*repo.CreateGitRepository}, headers, func(r gqlclient.GitRepositoryFragment) ([]string, error) { + return []string{r.ID, r.URL}, nil + }) +} + +func (p *Plural) handleUpdateCDRepository(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + repoId := c.Args().Get(0) + + attr := gqlclient.GitAttributes{ + URL: c.String("url"), + } + + if c.String("private-key") != "" { + attr.PrivateKey = lo.ToPtr(c.String("private-key")) + } + + if c.String("passphrase") != "" { + attr.Passphrase = lo.ToPtr(c.String("passphrase")) + } + + if c.String("password") != "" { + attr.Password = lo.ToPtr(c.String("password")) + } + + if c.String("username") != "" { + attr.Username = lo.ToPtr(c.String("username")) + } + + repo, err := p.ConsoleClient.UpdateRepository(repoId, attr) + if err != nil { + return err + } + + headers := []string{"ID", "URL"} + return utils.PrintTable([]gqlclient.GitRepositoryFragment{*repo.UpdateGitRepository}, headers, func(r gqlclient.GitRepositoryFragment) ([]string, error) { + return []string{r.ID, r.URL}, nil + }) +} + +func getFlag(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/cmd/plural/cd_services.go b/cmd/plural/cd_services.go new file mode 100644 index 00000000..1f4112bd --- /dev/null +++ b/cmd/plural/cd_services.go @@ -0,0 +1,418 @@ +package plural + +import ( + "fmt" + "strings" + + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/console" + "github.com/pluralsh/plural/pkg/utils" + "github.com/samber/lo" + "github.com/urfave/cli" + "k8s.io/apimachinery/pkg/util/yaml" +) + +func (p *Plural) cdServices() cli.Command { + return cli.Command{ + Name: "services", + Subcommands: p.cdServiceCommands(), + Usage: "manage CD services", + } +} + +func (p *Plural) cdServiceCommands() []cli.Command { + return []cli.Command{ + { + Name: "list", + ArgsUsage: "CLUSTER_ID", + Action: latestVersion(requireArgs(p.handleListClusterServices, []string{"CLUSTER_ID"})), + Usage: "list cluster services", + }, + { + Name: "create", + ArgsUsage: "CLUSTER_ID", + Flags: []cli.Flag{ + cli.StringFlag{Name: "name", Usage: "service name", Required: true}, + cli.StringFlag{Name: "namespace", Usage: "service namespace. If not specified the 'default' will be used"}, + cli.StringFlag{Name: "version", Usage: "service version. If not specified the '0.0.1' will be used"}, + cli.StringFlag{Name: "repo-id", Usage: "repository ID", Required: true}, + cli.StringFlag{Name: "git-ref", Usage: "git ref, can be branch, tag or commit sha", Required: true}, + cli.StringFlag{Name: "git-folder", Usage: "folder within the source tree where manifests are located", Required: true}, + cli.StringFlag{Name: "kustomize-folder", Usage: "folder within the kustomize file is located"}, + cli.StringSliceFlag{ + Name: "conf", + Usage: "config name value", + }, + cli.StringFlag{Name: "config-file", Usage: "path for configuration file"}, + }, + Action: latestVersion(requireArgs(p.handleCreateClusterService, []string{"CLUSTER_ID"})), + Usage: "create cluster service", + }, + { + Name: "update", + ArgsUsage: "SERVICE_ID", + Action: latestVersion(requireArgs(p.handleUpdateClusterService, []string{"SERVICE_ID"})), + Usage: "update cluster service", + Flags: []cli.Flag{ + cli.StringFlag{Name: "version", Usage: "service version"}, + cli.StringFlag{Name: "git-ref", Usage: "git ref, can be branch, tag or commit sha"}, + cli.StringFlag{Name: "git-folder", Usage: "folder within the source tree where manifests are located"}, + cli.StringFlag{Name: "kustomize-folder", Usage: "folder within the kustomize file is located"}, + cli.StringSliceFlag{ + Name: "conf", + Usage: "config name value", + }, + }, + }, + { + Name: "clone", + ArgsUsage: "CLUSTER SERVICE", + Action: latestVersion(requireArgs(p.handleCloneClusterService, []string{"CLUSTER", "SERVICE"})), + Flags: []cli.Flag{ + cli.StringFlag{Name: "name", Usage: "the name for the cloned service", Required: true}, + cli.StringFlag{Name: "namespace", Usage: "the namespace for this cloned service", Required: true}, + cli.StringSliceFlag{ + Name: "conf", + Usage: "config name value", + }, + }, + Usage: "deep clone a service onto either the same cluster or another", + }, + { + Name: "describe", + ArgsUsage: "SERVICE_ID", + Action: latestVersion(requireArgs(p.handleDescribeClusterService, []string{"SERVICE_ID"})), + Flags: []cli.Flag{ + cli.StringFlag{Name: "o", Usage: "output format"}, + }, + Usage: "describe cluster service", + }, + { + Name: "delete", + ArgsUsage: "SERVICE_ID", + Action: latestVersion(requireArgs(p.handleDeleteClusterService, []string{"SERVICE_ID"})), + Usage: "delete cluster service", + }, + } +} + +func (p *Plural) handleListClusterServices(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + sd, err := p.ConsoleClient.ListClusterServices(getIdAndName(c.Args().Get(0))) + if err != nil { + return err + } + if sd == nil { + return fmt.Errorf("returned objects list [ListClusterServices] is nil") + } + headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} + return utils.PrintTable(sd, headers, func(sd *gqlclient.ServiceDeploymentEdgeFragment) ([]string, error) { + return []string{sd.Node.ID, sd.Node.Name, sd.Node.Namespace, sd.Node.Git.Ref, sd.Node.Git.Folder, sd.Node.Repository.URL}, nil + }) +} + +func (p *Plural) handleCreateClusterService(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + v, err := validateFlag(c, "version", "0.0.1") + if err != nil { + return err + } + name := c.String("name") + namespace, err := validateFlag(c, "namespace", "default") + if err != nil { + return err + } + repoId := c.String("repo-id") + gitRef := c.String("git-ref") + gitFolder := c.String("git-folder") + attributes := gqlclient.ServiceDeploymentAttributes{ + Name: name, + Namespace: namespace, + Version: &v, + RepositoryID: repoId, + Git: gqlclient.GitRefAttributes{ + Ref: gitRef, + Folder: gitFolder, + }, + Configuration: []*gqlclient.ConfigAttributes{}, + } + + if c.String("kustomize-folder") != "" { + attributes.Kustomize = &gqlclient.KustomizeAttributes{ + Path: c.String("kustomize-folder"), + } + } + + if c.String("config-file") != "" { + configFile, err := utils.ReadFile(c.String("config-file")) + if err != nil { + return err + } + sdc := ServiceDeploymentAttributesConfiguration{} + if err := yaml.Unmarshal([]byte(configFile), &sdc); err != nil { + return err + } + attributes.Configuration = append(attributes.Configuration, sdc.Configuration...) + } + var confArgs []string + if c.IsSet("conf") { + confArgs = append(confArgs, c.StringSlice("conf")...) + } + for _, conf := range confArgs { + configurationPair := strings.Split(conf, "=") + if len(configurationPair) == 2 { + attributes.Configuration = append(attributes.Configuration, &gqlclient.ConfigAttributes{ + Name: configurationPair[0], + Value: &configurationPair[1], + }) + } + } + + clusterId, clusterName := getIdAndName(c.Args().Get(0)) + sd, err := p.ConsoleClient.CreateClusterService(clusterId, clusterName, attributes) + if err != nil { + return err + } + if sd == nil { + return fmt.Errorf("the returned object is empty, check if all fields are set") + } + + headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} + return utils.PrintTable([]*gqlclient.ServiceDeploymentFragment{sd}, headers, func(sd *gqlclient.ServiceDeploymentFragment) ([]string, error) { + return []string{sd.ID, sd.Name, sd.Namespace, sd.Git.Ref, sd.Git.Folder, sd.Repository.URL}, nil + }) +} + +func (p *Plural) handleCloneClusterService(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + cluster, err := p.ConsoleClient.GetCluster(getIdAndName(c.Args().Get(0))) + if err != nil { + return err + } + if cluster == nil { + return fmt.Errorf("could not find cluster %s", c.Args().Get(0)) + } + + serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(1)) + if err != nil { + return err + } + + attributes := gqlclient.ServiceCloneAttributes{ + Name: c.String("name"), + Namespace: lo.ToPtr(c.String("namespace")), + } + + // TODO: DRY this up with service update + var confArgs []string + if c.IsSet("conf") { + confArgs = append(confArgs, c.StringSlice("conf")...) + } + + updateConfigurations := map[string]string{} + for _, conf := range confArgs { + configurationPair := strings.Split(conf, "=") + if len(configurationPair) == 2 { + updateConfigurations[configurationPair[0]] = configurationPair[1] + } + } + for key, value := range updateConfigurations { + attributes.Configuration = append(attributes.Configuration, &gqlclient.ConfigAttributes{ + Name: key, + Value: lo.ToPtr(value), + }) + } + + sd, err := p.ConsoleClient.CloneService(cluster.ID, serviceId, serviceName, clusterName, attributes) + if err != nil { + return err + } + + headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} + return utils.PrintTable([]*gqlclient.ServiceDeploymentFragment{sd}, headers, func(sd *gqlclient.ServiceDeploymentFragment) ([]string, error) { + return []string{sd.ID, sd.Name, sd.Namespace, sd.Git.Ref, sd.Git.Folder, sd.Repository.URL}, nil + }) +} + +func (p *Plural) handleUpdateClusterService(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0)) + if err != nil { + return err + } + + existing, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) + if err != nil { + return err + } + if existing == nil { + return fmt.Errorf("existing service deployment is empty") + } + existingConfigurations := map[string]string{} + attributes := gqlclient.ServiceUpdateAttributes{ + Version: &existing.Version, + Git: &gqlclient.GitRefAttributes{ + Ref: existing.Git.Ref, + Folder: existing.Git.Folder, + }, + Configuration: []*gqlclient.ConfigAttributes{}, + } + if existing.Kustomize != nil { + attributes.Kustomize = &gqlclient.KustomizeAttributes{ + Path: existing.Kustomize.Path, + } + } + + for _, conf := range existing.Configuration { + existingConfigurations[conf.Name] = conf.Value + } + + v := c.String("version") + if v != "" { + attributes.Version = &v + } + if c.String("git-ref") != "" { + attributes.Git.Ref = c.String("git-ref") + } + if c.String("git-folder") != "" { + attributes.Git.Folder = c.String("git-folder") + } + var confArgs []string + if c.IsSet("conf") { + confArgs = append(confArgs, c.StringSlice("conf")...) + } + + updateConfigurations := map[string]string{} + for _, conf := range confArgs { + configurationPair := strings.Split(conf, "=") + if len(configurationPair) == 2 { + updateConfigurations[configurationPair[0]] = configurationPair[1] + } + } + for k, v := range updateConfigurations { + existingConfigurations[k] = v + } + for key, value := range existingConfigurations { + attributes.Configuration = append(attributes.Configuration, &gqlclient.ConfigAttributes{ + Name: key, + Value: lo.ToPtr(value), + }) + } + if c.String("kustomize-folder") != "" { + attributes.Kustomize = &gqlclient.KustomizeAttributes{ + Path: c.String("kustomize-folder"), + } + } + sd, err := p.ConsoleClient.UpdateClusterService(serviceId, serviceName, clusterName, attributes) + if err != nil { + return err + } + if sd == nil { + return fmt.Errorf("returned object is nil") + } + + headers := []string{"Id", "Name", "Namespace", "Git Ref", "Git Folder", "Repo"} + return utils.PrintTable([]*gqlclient.ServiceDeploymentFragment{sd}, headers, func(sd *gqlclient.ServiceDeploymentFragment) ([]string, error) { + return []string{sd.ID, sd.Name, sd.Namespace, sd.Git.Ref, sd.Git.Folder, sd.Repository.URL}, nil + }) +} + +func (p *Plural) handleDescribeClusterService(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0)) + if err != nil { + return err + } + existing, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) + if err != nil { + return err + } + if existing == nil { + return fmt.Errorf("existing service deployment is empty") + } + output := c.String("o") + if output == "json" { + utils.NewJsonPrinter(existing).PrettyPrint() + return nil + } else if output == "yaml" { + utils.NewYAMLPrinter(existing).PrettyPrint() + return nil + } + desc, err := console.DescribeService(existing) + if err != nil { + return err + } + fmt.Print(desc) + + return nil +} + +func (p *Plural) handleDeleteClusterService(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0)) + if err != nil { + return err + } + + svc, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) + if err != nil { + return err + } + if svc == nil { + return fmt.Errorf("Could not find service for %s", c.Args().Get(0)) + } + + deleted, err := p.ConsoleClient.DeleteClusterService(svc.ID) + if err != nil { + return fmt.Errorf("could not delete service: %w", err) + } + + utils.Success("Service %s has been deleted successfully\n", deleted.DeleteServiceDeployment.Name) + return nil +} + +type ServiceDeploymentAttributesConfiguration struct { + Configuration []*gqlclient.ConfigAttributes +} + +func getServiceIdClusterNameServiceName(input string) (serviceId, clusterName, serviceName *string, err error) { + if strings.HasPrefix(input, "@") { + i := strings.Trim(input, "@") + split := strings.Split(i, "/") + if len(split) != 2 { + err = fmt.Errorf("expected format @clusterName/serviceName") + return + } + clusterName = &split[0] + serviceName = &split[1] + } else { + serviceId = &input + } + return +} + +func validateFlag(ctx *cli.Context, name string, defaultVal string) (string, error) { + res := ctx.String(name) + if res == "" { + if defaultVal == "" { + return "", fmt.Errorf("expected --%s flag", name) + } + res = defaultVal + } + + return res, nil +} diff --git a/cmd/plural/cd_test.go b/cmd/plural/cd_test.go new file mode 100644 index 00000000..0ce04d38 --- /dev/null +++ b/cmd/plural/cd_test.go @@ -0,0 +1,177 @@ +package plural_test + +import ( + "os" + "testing" + + consoleclient "github.com/pluralsh/console-client-go" + plural "github.com/pluralsh/plural/cmd/plural" + "github.com/pluralsh/plural/pkg/test/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestListCDClusters(t *testing.T) { + tests := []struct { + name string + args []string + expectedResponse string + result *consoleclient.ListClusters + }{ + { + name: `test "deployments clusters list" when returns nil object`, + result: nil, + args: []string{plural.ApplicationName, "deployments", "clusters", "list"}, + expectedResponse: `returned objects list [ListClusters] is nil`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := mocks.NewConsoleClient(t) + client.On("ListClusters").Return(test.result, nil) + app := plural.CreateNewApp(&plural.Plural{ + Client: nil, + ConsoleClient: client, + Kube: nil, + HelmConfiguration: nil, + }) + app.HelpName = plural.ApplicationName + os.Args = test.args + _, err := captureStdout(app, os.Args) + + assert.Error(t, err) + assert.Equal(t, test.expectedResponse, err.Error()) + }) + } +} + +func TestDescribeCDCluster(t *testing.T) { + tests := []struct { + name string + args []string + expectedResponse string + expectedError string + result *consoleclient.ClusterFragment + }{ + { + name: `test "deployments clusters describe" when returns nil`, + result: nil, + args: []string{plural.ApplicationName, "deployments", "clusters", "describe", "abc"}, + expectedError: `existing cluster is empty`, + }, + { + name: `test "deployments clusters describe"`, + result: &consoleclient.ClusterFragment{ + ID: "abc", + Name: "test", + }, + args: []string{plural.ApplicationName, "deployments", "clusters", "describe", "abc"}, + expectedResponse: `Id: abc +Name: test +`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := mocks.NewConsoleClient(t) + client.On("GetCluster", mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.result, nil) + app := plural.CreateNewApp(&plural.Plural{ + Client: nil, + ConsoleClient: client, + Kube: nil, + HelmConfiguration: nil, + }) + app.HelpName = plural.ApplicationName + os.Args = test.args + out, err := captureStdout(app, os.Args) + + if test.expectedError != "" { + assert.Error(t, err) + assert.Equal(t, test.expectedError, err.Error()) + } + if test.expectedResponse != "" { + assert.Equal(t, test.expectedResponse, out) + } + }) + } +} + +func TestListCDRepositories(t *testing.T) { + tests := []struct { + name string + args []string + expectedResponse string + expectedError string + result *consoleclient.ListGitRepositories + }{ + { + name: `test "deployments repositories list" when returns nil`, + result: nil, + args: []string{plural.ApplicationName, "deployments", "repositories", "list"}, + expectedError: `returned objects list [ListRepositories] is nil`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := mocks.NewConsoleClient(t) + client.On("ListRepositories").Return(test.result, nil) + app := plural.CreateNewApp(&plural.Plural{ + Client: nil, + ConsoleClient: client, + Kube: nil, + HelmConfiguration: nil, + }) + app.HelpName = plural.ApplicationName + os.Args = test.args + out, err := captureStdout(app, os.Args) + + if test.expectedError != "" { + assert.Error(t, err) + assert.Equal(t, test.expectedError, err.Error()) + } + if test.expectedResponse != "" { + assert.Equal(t, test.expectedResponse, out) + } + }) + } +} + +func TestListCDServices(t *testing.T) { + tests := []struct { + name string + args []string + expectedResponse string + expectedError string + result []*consoleclient.ServiceDeploymentEdgeFragment + }{ + { + name: `test "deployments services list" when returns nil`, + result: nil, + args: []string{plural.ApplicationName, "deployments", "services", "list", "clusterID"}, + expectedError: `returned objects list [ListClusterServices] is nil`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := mocks.NewConsoleClient(t) + client.On("ListClusterServices", mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.result, nil) + app := plural.CreateNewApp(&plural.Plural{ + Client: nil, + ConsoleClient: client, + Kube: nil, + HelmConfiguration: nil, + }) + app.HelpName = plural.ApplicationName + os.Args = test.args + out, err := captureStdout(app, os.Args) + + if test.expectedError != "" { + assert.Error(t, err) + assert.Equal(t, test.expectedError, err.Error()) + } + if test.expectedResponse != "" { + assert.Equal(t, test.expectedResponse, out) + } + }) + } +} diff --git a/cmd/plural/config_test.go b/cmd/plural/config_test.go index 319db08e..e4f3b3bb 100644 --- a/cmd/plural/config_test.go +++ b/cmd/plural/config_test.go @@ -24,13 +24,13 @@ func TestPluralConfigCommand(t *testing.T) { { name: `test "config read" command when config file doesn't exists'`, args: []string{plural.ApplicationName, "config", "read"}, - expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: \"\"\n token: \"\"\n namespacePrefix: \"\"\n endpoint: \"\"\n lockProfile: \"\"\n reportErrors: false\n", + expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: \"\"\n token: \"\"\n consoleToken: \"\"\n namespacePrefix: \"\"\n endpoint: \"\"\n lockProfile: \"\"\n reportErrors: false\n", }, { name: `test "config read" command with default test config'`, args: []string{plural.ApplicationName, "config", "read"}, createConfig: true, - expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: test@plural.sh\n token: abc\n namespacePrefix: test\n endpoint: http://example.com\n lockProfile: abc\n reportErrors: false\n", + expectedResponse: "apiVersion: platform.plural.sh/v1alpha1\nkind: Config\nmetadata: null\nspec:\n email: test@plural.sh\n token: abc\n consoleToken: \"\"\n namespacePrefix: test\n endpoint: http://example.com\n lockProfile: abc\n reportErrors: false\n", }, } for _, test := range tests { diff --git a/cmd/plural/plural.go b/cmd/plural/plural.go index 305d8555..6b3c7b85 100644 --- a/cmd/plural/plural.go +++ b/cmd/plural/plural.go @@ -1,18 +1,19 @@ package plural import ( + "fmt" "os" - "github.com/urfave/cli" - "helm.sh/helm/v3/pkg/action" - "github.com/pluralsh/plural/pkg/api" "github.com/pluralsh/plural/pkg/config" + "github.com/pluralsh/plural/pkg/console" "github.com/pluralsh/plural/pkg/crypto" "github.com/pluralsh/plural/pkg/exp" "github.com/pluralsh/plural/pkg/kubernetes" "github.com/pluralsh/plural/pkg/manifest" "github.com/pluralsh/plural/pkg/utils" + "github.com/urfave/cli" + "helm.sh/helm/v3/pkg/action" ) func init() { @@ -23,6 +24,7 @@ const ApplicationName = "plural" type Plural struct { api.Client + ConsoleClient console.ConsoleClient kubernetes.Kube HelmConfiguration *action.Configuration } @@ -38,6 +40,26 @@ func (p *Plural) InitKube() error { return nil } +func (p *Plural) InitConsoleClient(token, url string) error { + if p.ConsoleClient == nil { + if token == "" { + conf := console.ReadConfig() + if conf.Token == "" { + return fmt.Errorf("you have not set up a console login, you can run `plural cd login` to save your credentials") + } + + token = conf.Token + url = conf.Url + } + consoleClient, err := console.NewConsoleClient(token, url) + if err != nil { + return err + } + p.ConsoleClient = consoleClient + } + return nil +} + func (p *Plural) assumeServiceAccount(conf config.Config, man *manifest.ProjectManifest) error { owner := man.Owner jwt, email, err := api.FromConfig(&conf).ImpersonateServiceAccount(owner.Email) @@ -439,6 +461,27 @@ func (p *Plural) getCommands() []cli.Command { Action: p.aiHelp, Category: "Debugging", }, + { + Name: "deployments", + Aliases: []string{"cd"}, + Usage: "view and manage plural deployments", + Subcommands: p.cdCommands(), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "token", + Usage: "console token", + EnvVar: "PLURAL_CONSOLE_TOKEN", + Destination: &consoleToken, + }, + cli.StringFlag{ + Name: "url", + Usage: "console url address", + EnvVar: "PLURAL_CONSOLE_URL", + Destination: &consoleURL, + }, + }, + Category: "CD", + }, { Name: "template", Aliases: []string{"tpl"}, diff --git a/go.mod b/go.mod index 3b69adb8..ec169237 100644 --- a/go.mod +++ b/go.mod @@ -50,13 +50,15 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/norwoodj/helm-docs v1.11.2 github.com/olekukonko/tablewriter v0.0.5 + github.com/osteele/liquid v1.3.2 github.com/packethost/packngo v0.29.0 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pluralsh/cluster-api-migration v0.2.15 - github.com/pluralsh/gqlclient v1.10.0 + github.com/pluralsh/console-client-go v0.0.34 + github.com/pluralsh/gqlclient v1.11.0 github.com/pluralsh/plural-operator v0.5.5 github.com/pluralsh/polly v0.1.1 - github.com/pluralsh/terraform-delinker v0.0.1 + github.com/pluralsh/terraform-delinker v0.0.2 github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69 github.com/rivo/tview v0.0.0-20230615085408-bb9595ee0f4d github.com/rodaine/hclencoder v0.0.1 @@ -219,6 +221,7 @@ require ( github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect github.com/onsi/gomega v1.27.6 // indirect github.com/orcaman/concurrent-map v1.0.0 // indirect + github.com/osteele/tuesday v1.0.3 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect diff --git a/go.sum b/go.sum index ae80cf13..36cb87d0 100644 --- a/go.sum +++ b/go.sum @@ -1380,6 +1380,10 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/osteele/liquid v1.3.2 h1:G+MvVYt1HX2xuv99JgdrhV7zRVdlvFnNi8M5rN8gQmI= +github.com/osteele/liquid v1.3.2/go.mod h1:VmzQQHa5v4E0GvGzqccfAfLgMwRk2V+s1QbxYx9dGak= +github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU= +github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -1419,10 +1423,14 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pluralsh/cluster-api-migration v0.2.15 h1:TIfusD+wnhZTGmwNfIlKlKJOT2dE3rUaZawDJw98GjY= github.com/pluralsh/cluster-api-migration v0.2.15/go.mod h1:J6lEvC/70KouikX16mE331cxc3y3sBwtmfHGwZqu06w= +github.com/pluralsh/console-client-go v0.0.33 h1:Bmh5CRBIYyb5wfYlrAntqQZ3GduBTUmzJlmCeo4HAWU= +github.com/pluralsh/console-client-go v0.0.33/go.mod h1:kZjk0pXAWnvyj+miXveCho4kKQaX1Tm3CGAM+iwurWU= +github.com/pluralsh/console-client-go v0.0.34 h1:YEvLvwE9s7xGClNiIPPVpISZ9/8RUpwvTWeF0w0u2q0= +github.com/pluralsh/console-client-go v0.0.34/go.mod h1:kZjk0pXAWnvyj+miXveCho4kKQaX1Tm3CGAM+iwurWU= github.com/pluralsh/controller-reconcile-helper v0.0.4 h1:1o+7qYSyoeqKFjx+WgQTxDz4Q2VMpzprJIIKShxqG0E= github.com/pluralsh/controller-reconcile-helper v0.0.4/go.mod h1:AfY0gtteD6veBjmB6jiRx/aR4yevEf6K0M13/pGan/s= -github.com/pluralsh/gqlclient v1.10.0 h1:ccYB+A0JbPYkEeVzdfajd29l65N6x/buSKPMMxM8OIA= -github.com/pluralsh/gqlclient v1.10.0/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= +github.com/pluralsh/gqlclient v1.11.0 h1:FfXW7FiEJLHOfTAa7NxDb8jb3aMZNIpCAcG+bg8uHYA= +github.com/pluralsh/gqlclient v1.11.0/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= github.com/pluralsh/helm-docs v1.11.3-0.20230914191425-6d14ebab8817 h1:J7SGxH6nJGdRoNtqdzhyr2VMpbl4asolul7xqqW++EA= github.com/pluralsh/helm-docs v1.11.3-0.20230914191425-6d14ebab8817/go.mod h1:rLqec59NO7YF57Rq9VlubQHMp7wcRTJhzpkcgs4lOG4= github.com/pluralsh/oauth v0.9.2 h1:tM9hBK4tCnJUeCOgX0ctxBBCS3hiCDPoxkJLODtedmQ= @@ -1431,8 +1439,8 @@ github.com/pluralsh/plural-operator v0.5.5 h1:57GxniNjUa3hpHgvFr9oDonFgvDUC8XDD5 github.com/pluralsh/plural-operator v0.5.5/go.mod h1:WIXiz26/WDcUn0FA7Q1jPxmfsm98U1/JL8YpIdKVLX0= github.com/pluralsh/polly v0.1.1 h1:VtbS83re2YuDgscvaFgfzEDZs9uXRV84fCdvKCgIRE4= github.com/pluralsh/polly v0.1.1/go.mod h1:Yo1/jcW+4xwhWG+ZJikZy4J4HJkMNPZ7sq5auL2c/tY= -github.com/pluralsh/terraform-delinker v0.0.1 h1:I0cnf5IqY94qJWzFwPaRsrFp0s3gR+YnYKodYfUSMvA= -github.com/pluralsh/terraform-delinker v0.0.1/go.mod h1:mU4F5OtfAIG5Xobbk+3uiU++AWKyfZPyyMmVZd23bV4= +github.com/pluralsh/terraform-delinker v0.0.2 h1:8SbUVxQa5To13ZZV2H64JLDLMEKolZWsqC+osyaTAu0= +github.com/pluralsh/terraform-delinker v0.0.2/go.mod h1:mU4F5OtfAIG5Xobbk+3uiU++AWKyfZPyyMmVZd23bV4= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/hack/e2e/setup-plural.sh b/hack/e2e/setup-plural.sh index 2222194b..b24ff24c 100755 --- a/hack/e2e/setup-plural.sh +++ b/hack/e2e/setup-plural.sh @@ -114,7 +114,7 @@ export PLURAL_DISABLE_MP_TABLE_VIEW=true INSTALL_APP=${INSTALL_APP:-"bootstrap"} INSTALL_RECIPE=${INSTALL_RECIPE:-"docker-cluster-api-simple-test"} - +export EXP_PLURAL_CAPI=true plural init plural repos reset plural bundle install "$INSTALL_APP" "$INSTALL_RECIPE" diff --git a/hack/gen-client-mocks.sh b/hack/gen-client-mocks.sh index 63ee3e6d..8d1ae526 100755 --- a/hack/gen-client-mocks.sh +++ b/hack/gen-client-mocks.sh @@ -6,7 +6,8 @@ cd $(dirname $0)/.. source hack/lib.sh -CONTAINERIZE_IMAGE=golang:1.18.4 containerize ./hack/gen-client-mocks.sh +CONTAINERIZE_IMAGE=golang:1.21.1 containerize ./hack/gen-client-mocks.sh go run github.com/vektra/mockery/v2@latest --dir=pkg/api/ --name=Client --output=pkg/test/mocks go run github.com/vektra/mockery/v2@latest --dir=pkg/kubernetes --name=Kube --output=pkg/test/mocks +go run github.com/vektra/mockery/v2@latest --dir=pkg/console --name=ConsoleClient --output=pkg/test/mocks \ No newline at end of file diff --git a/hack/pipeline.yaml b/hack/pipeline.yaml new file mode 100644 index 00000000..bf2cc228 --- /dev/null +++ b/hack/pipeline.yaml @@ -0,0 +1,15 @@ +name: test +stages: +- name: dev + services: + - name: cd-demo/guestbook +- name: prod + services: + - name: cd-demo-workload-1/guestbook + criteria: + source: cd-demo/guestbook + secrets: + - test +edges: +- from: dev + to: prod \ No newline at end of file diff --git a/pkg/api/client.go b/pkg/api/client.go index a4e0c30d..8e0e9097 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -59,6 +59,7 @@ type Client interface { GetPackageInstallations(repoId string) (charts []*ChartInstallation, tfs []*TerraformInstallation, err error) CreateCrd(repo string, chart string, file string) error CreateDomain(name string) error + CreateInstallation(id string) (string, error) GetInstallation(name string) (*Installation, error) GetInstallationById(id string) (*Installation, error) GetInstallations() ([]*Installation, error) diff --git a/pkg/api/installations.go b/pkg/api/installations.go index b0127f1a..96c8054c 100644 --- a/pkg/api/installations.go +++ b/pkg/api/installations.go @@ -39,6 +39,14 @@ func (client *client) DeleteInstallation(id string) error { return err } +func (client *client) CreateInstallation(id string) (string, error) { + resp, err := client.pluralClient.CreateInstallation(client.ctx, id) + if err != nil { + return "", err + } + return resp.CreateInstallation.ID, err +} + func convertInstallation(installation *gqlclient.InstallationFragment) *Installation { if installation == nil { return nil diff --git a/pkg/bootstrap/cilium.go b/pkg/bootstrap/cilium.go index f8c84fbc..2ffc878f 100644 --- a/pkg/bootstrap/cilium.go +++ b/pkg/bootstrap/cilium.go @@ -1,26 +1,16 @@ package bootstrap import ( - "context" - "fmt" - "os" "os/exec" - "path/filepath" - "strings" "time" - "github.com/gofrs/flock" "github.com/pkg/errors" "github.com/pluralsh/plural/pkg/helm" "github.com/pluralsh/plural/pkg/utils" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/helmpath" - "helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v3/pkg/storage/driver" - "sigs.k8s.io/yaml" ) var settings = cli.New() @@ -37,7 +27,7 @@ func installCilium(cluster string) error { return err } - if err := addCiliumRepo(); err != nil { + if err := helm.AddRepo(ciliumRepoName, ciliumRepoUrl); err != nil { return err } @@ -74,67 +64,3 @@ func installCilium(cluster string) error { return err } - -func addCiliumRepo() error { - repoFile := getEnvVar("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")) - err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm) - if err != nil && !os.IsExist(err) { - return err - } - - // Acquire a file lock for process synchronization. - repoFileExt := filepath.Ext(repoFile) - var lockPath string - if len(repoFileExt) > 0 && len(repoFileExt) < len(repoFile) { - lockPath = strings.TrimSuffix(repoFile, repoFileExt) + ".lock" - } else { - lockPath = repoFile + ".lock" - } - fileLock := flock.New(lockPath) - lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - locked, err := fileLock.TryLockContext(lockCtx, time.Second) - if err == nil && locked { - defer func(fileLock *flock.Flock) { - _ = fileLock.Unlock() - }(fileLock) - } - if err != nil { - return err - } - - b, err := os.ReadFile(repoFile) - if err != nil && !os.IsNotExist(err) { - return err - } - - var f repo.File - if err := yaml.Unmarshal(b, &f); err != nil { - return err - } - - c := repo.Entry{ - Name: ciliumRepoName, - URL: ciliumRepoUrl, - InsecureSkipTLSverify: true, - } - - // If the repo exists do one of two things: - // 1. If the configuration for the name is the same continue without error. - // 2. When the config is different require --force-update. - if f.Has(ciliumRepoName) { - return nil - } - - r, err := repo.NewChartRepository(&c, getter.All(settings)) - if err != nil { - return err - } - - if _, err := r.DownloadIndexFile(); err != nil { - return fmt.Errorf("looks like %q is not a valid chart repository or cannot be reached", ciliumRepoUrl) - } - - f.Update(&c) - return f.WriteFile(repoFile, 0644) -} diff --git a/pkg/bootstrap/common.go b/pkg/bootstrap/common.go index e2ecefb3..e66e0426 100644 --- a/pkg/bootstrap/common.go +++ b/pkg/bootstrap/common.go @@ -119,15 +119,6 @@ func moveHelmSecrets(sourceContext, targetContext string) error { return createSecrets(targetContext, secrets.Items) } -// getEnvVar gets value of environment variable, if it is not set then default value is returned instead. -func getEnvVar(name, defaultValue string) string { - if v, ok := os.LookupEnv(name); ok { - return v - } - - return defaultValue -} - // getBootstrapFlags returns list of provider-specific flags used during cluster bootstrap and destroy. func getBootstrapFlags(prov string) []string { switch prov { @@ -153,7 +144,6 @@ func getBootstrapFlags(prov string) []string { } case api.ProviderGCP: return []string{ - "--set", "bootstrap.cert-manager.serviceAccount.create=true", "--set", "cluster-api-provider-gcp.cluster-api-provider-gcp.bootstrapMode=true", "--set", "bootstrap.external-dns.enabled=false", "--set", "plural-certmanager-webhook.enabled=false", @@ -321,7 +311,7 @@ func RunWithTempCredentials(function ActionFunc) error { } flags = []string{ - "--setJSON", fmt.Sprintf(`cluster-api-provider-gcp.cluster-api-provider-gcp.managerBootstrapCredentials.credentialsJson=%q`, string(credentials.JSON)), + "--setJSON", fmt.Sprintf(`cluster-api-provider-gcp.cluster-api-provider-gcp.managerBootstrapCredentials.credentialsJson=%s`, string(credentials.JSON)), } } diff --git a/pkg/bundle/oidc.go b/pkg/bundle/oidc.go index 9d8f538a..be11f622 100644 --- a/pkg/bundle/oidc.go +++ b/pkg/bundle/oidc.go @@ -55,6 +55,29 @@ func ConfigureOidc(repo string, client api.Client, recipe *api.Recipe, ctx map[s return api.GetErrorResponse(err, "OIDCProvider") } +func SetupOIDC(repo string, client api.Client, redirectUris []string, authMethod string) error { + inst, err := client.GetInstallation(repo) + if err != nil { + return api.GetErrorResponse(err, "GetInstallation") + } + + me, err := client.Me() + if err != nil { + return api.GetErrorResponse(err, "Me") + } + + oidcSettings := &api.OidcProviderAttributes{ + RedirectUris: redirectUris, + AuthMethod: authMethod, + Bindings: []api.Binding{ + {UserId: me.Id}, + }, + } + mergeOidcAttributes(inst, oidcSettings) + err = client.OIDCProvider(inst.Id, oidcSettings) + return api.GetErrorResponse(err, "OIDCProvider") +} + func mergeOidcAttributes(inst *api.Installation, attributes *api.OidcProviderAttributes) { if inst.OIDCProvider == nil { return diff --git a/pkg/cd/clusters_create.go b/pkg/cd/clusters_create.go new file mode 100644 index 00000000..5c956f79 --- /dev/null +++ b/pkg/cd/clusters_create.go @@ -0,0 +1,113 @@ +package cd + +import ( + "fmt" + + "github.com/AlecAivazis/survey/v2" + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/api" +) + +func AskCloudSettings(provider string) (*gqlclient.CloudSettingsAttributes, error) { + switch provider { + case api.ProviderAWS: + if acs, err := askAWSCloudSettings(); err != nil { + return nil, err + } else { + return &gqlclient.CloudSettingsAttributes{Aws: acs}, nil + } + case api.ProviderAzure: + if acs, err := askAzureCloudSettings(); err != nil { + return nil, err + } else { + return &gqlclient.CloudSettingsAttributes{Azure: acs}, nil + } + case api.ProviderGCP: + if gcs, err := askGCPCloudSettings(); err != nil { + return nil, err + } else { + return &gqlclient.CloudSettingsAttributes{Gcp: gcs}, nil + } + } + + return nil, fmt.Errorf("unknown provider") +} + +func askAWSCloudSettings() (*gqlclient.AwsCloudAttributes, error) { + region := "" + prompt := &survey.Input{ + Message: "Enter AWS region:", + } + if err := survey.AskOne(prompt, ®ion, survey.WithValidator(survey.Required)); err != nil { + return nil, err + } + return &gqlclient.AwsCloudAttributes{ + Region: ®ion, + }, nil +} + +func askAzureCloudSettings() (*gqlclient.AzureCloudAttributes, error) { + azureSurvey := []*survey.Question{ + { + Name: "location", + Prompt: &survey.Input{Message: "Enter the location:"}, + }, + { + Name: "subscription", + Prompt: &survey.Input{Message: "Enter the subscription ID:"}, + }, + { + Name: "resource", + Prompt: &survey.Input{Message: "Enter the resource group:"}, + }, + { + Name: "network", + Prompt: &survey.Input{Message: "Enter the network name:"}, + }, + } + var resp struct { + Location string + Subscription string + Resource string + Network string + } + if err := survey.Ask(azureSurvey, &resp); err != nil { + return nil, err + } + return &gqlclient.AzureCloudAttributes{ + Location: &resp.Location, + SubscriptionID: &resp.Subscription, + ResourceGroup: &resp.Resource, + Network: &resp.Network, + }, nil +} + +func askGCPCloudSettings() (*gqlclient.GcpCloudAttributes, error) { + awsSurvey := []*survey.Question{ + { + Name: "project", + Prompt: &survey.Input{Message: "Enter the project name:"}, + }, + { + Name: "network", + Prompt: &survey.Input{Message: "Enter the network name:"}, + }, + { + Name: "region", + Prompt: &survey.Input{Message: "Enter the region:"}, + }, + } + var resp struct { + Project string + Network string + Region string + } + if err := survey.Ask(awsSurvey, &resp); err != nil { + return nil, err + } + return &gqlclient.GcpCloudAttributes{ + Project: &resp.Project, + Network: &resp.Network, + Region: &resp.Region, + }, nil +} diff --git a/pkg/cd/clusters_get_credentials.go b/pkg/cd/clusters_get_credentials.go new file mode 100644 index 00000000..01d3e44f --- /dev/null +++ b/pkg/cd/clusters_get_credentials.go @@ -0,0 +1,52 @@ +package cd + +import ( + "fmt" + + gqlclient "github.com/pluralsh/console-client-go" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func SaveClusterKubeconfig(cluster *gqlclient.ClusterFragment, token string) error { + configAccess := clientcmd.NewDefaultPathOptions() + config, err := configAccess.GetStartingConfig() + if err != nil { + return fmt.Errorf("cannot read kubeconfig: %w", err) + } + if config == nil { + config = &clientcmdapi.Config{} + } + + // TODO: We should additionally set CertificateAuthority for Cluster. + configCluster := clientcmdapi.NewCluster() + configCluster.Server = *cluster.KasURL + if config.Clusters == nil { + config.Clusters = make(map[string]*clientcmdapi.Cluster) + } + config.Clusters[cluster.Name] = configCluster + + configAuthInfo := clientcmdapi.NewAuthInfo() + configAuthInfo.Token = fmt.Sprintf("plrl:%s:%s", cluster.ID, token) + if config.AuthInfos == nil { + config.AuthInfos = make(map[string]*clientcmdapi.AuthInfo) + } + config.AuthInfos[cluster.Name] = configAuthInfo + + configContext := clientcmdapi.NewContext() + configContext.Cluster = cluster.Name + configContext.AuthInfo = cluster.Name + if config.Contexts == nil { + config.Contexts = make(map[string]*clientcmdapi.Context) + } + config.Contexts[cluster.Name] = configContext + + config.CurrentContext = cluster.Name + + if err := clientcmd.ModifyConfig(configAccess, *config, true); err != nil { + return err + } + + fmt.Printf("set your kubectl context to %s\n", cluster.Name) + return nil +} diff --git a/pkg/cd/control_plane_install.go b/pkg/cd/control_plane_install.go new file mode 100644 index 00000000..054b1c92 --- /dev/null +++ b/pkg/cd/control_plane_install.go @@ -0,0 +1,140 @@ +package cd + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "github.com/AlecAivazis/survey/v2" + "github.com/osteele/liquid" + "github.com/pluralsh/plural/pkg/api" + "github.com/pluralsh/plural/pkg/bundle" + "github.com/pluralsh/plural/pkg/config" + "github.com/pluralsh/plural/pkg/crypto" + "github.com/pluralsh/plural/pkg/utils" +) + +var ( + liquidEngine = liquid.NewEngine() +) + +const ( + templateUrl = "https://raw.githubusercontent.com/pluralsh/console/cd-scaffolding/charts/console/values.yaml.liquid" +) + +func CreateControlPlane(conf config.Config) (string, error) { + client := api.FromConfig(&conf) + me, err := client.Me() + if err != nil { + return "", fmt.Errorf("you must run `plural login` before installing") + } + + azureSurvey := []*survey.Question{ + { + Name: "console", + Prompt: &survey.Input{Message: "Enter a dns name for your installation of the console (eg console.your.domain):"}, + }, + { + Name: "kubeProxy", + Prompt: &survey.Input{Message: "Enter a dns name for the kube proxy (eg kas.your.domain), this is used for dashboarding functionality:"}, + }, + { + Name: "clusterName", + Prompt: &survey.Input{Message: "Enter a name for this cluster:"}, + }, + { + Name: "postgresDsn", + Prompt: &survey.Input{Message: "Enter a postgres connection string for the underlying database (should be postgres://:@:5432/):"}, + }, + } + var resp struct { + Console string + KubeProxy string + ClusterName string + PostgresDsn string + } + if err := survey.Ask(azureSurvey, &resp); err != nil { + return "", err + } + + randoms := map[string]string{} + for _, key := range []string{"jwt", "erlang", "adminPassword", "kasApi", "kasPrivateApi", "kasRedis"} { + rand, err := crypto.RandStr(32) + if err != nil { + return "", err + } + randoms[key] = rand + } + + configuration := map[string]string{ + "consoleDns": resp.Console, + "kasDns": resp.KubeProxy, + "aesKey": utils.GenAESKey(), + "adminName": me.Email, + "adminEmail": me.Email, + "clusterName": resp.ClusterName, + "pluralToken": conf.Token, + "postgresUrl": resp.PostgresDsn, + } + for k, v := range randoms { + configuration[k] = v + } + + clientId, clientSecret, err := ensureInstalledAndOidc(client, resp.Console) + if err != nil { + return "", err + } + configuration["pluralClientId"] = clientId + configuration["pluralClientSecret"] = clientSecret + + tpl, err := fetchTemplate() + if err != nil { + return "", err + } + + bindings := map[string]interface{}{ + "configuration": configuration, + } + + res, err := liquidEngine.ParseAndRender(tpl, bindings) + return string(res), err +} + +func fetchTemplate() (res []byte, err error) { + resp, err := http.Get(templateUrl) + if err != nil { + return + } + defer resp.Body.Close() + var out bytes.Buffer + _, err = io.Copy(&out, resp.Body) + return out.Bytes(), err +} + +func ensureInstalledAndOidc(client api.Client, dns string) (clientId string, clientSecret string, err error) { + inst, err := client.GetInstallation("console") + if err != nil || inst == nil { + repo, err := client.GetRepository("console") + if err != nil { + return "", "", err + } + _, err = client.CreateInstallation(repo.Id) + if err != nil { + return "", "", err + } + } + + redirectUris := []string{fmt.Sprintf("https://%s/oauth/callback", dns)} + err = bundle.SetupOIDC("console", client, redirectUris, "POST") + if err != nil { + return + } + + inst, err = client.GetInstallation("console") + if err != nil { + return + } + + return inst.OIDCProvider.ClientId, inst.OIDCProvider.ClientSecret, nil +} diff --git a/pkg/cd/eject.go b/pkg/cd/eject.go new file mode 100644 index 00000000..c6902291 --- /dev/null +++ b/pkg/cd/eject.go @@ -0,0 +1,9 @@ +package cd + +import ( + gqlclient "github.com/pluralsh/console-client-go" +) + +func Eject(_ *gqlclient.ClusterFragment) error { + return nil +} diff --git a/pkg/cd/providers_create.go b/pkg/cd/providers_create.go new file mode 100644 index 00000000..202a2f86 --- /dev/null +++ b/pkg/cd/providers_create.go @@ -0,0 +1,148 @@ +package cd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/AlecAivazis/survey/v2" + gqlclient "github.com/pluralsh/console-client-go" + + "github.com/pluralsh/plural/pkg/api" +) + +func AskCloudProviderSettings(provider string) (*gqlclient.CloudProviderSettingsAttributes, error) { + switch provider { + case api.ProviderAWS: + if acs, err := askAWSCloudProviderSettings(); err != nil { + return nil, err + } else { + return &gqlclient.CloudProviderSettingsAttributes{Aws: acs}, nil + } + case api.ProviderAzure: + if acs, err := askAzureCloudProviderSettings(); err != nil { + return nil, err + } else { + return &gqlclient.CloudProviderSettingsAttributes{Azure: acs}, nil + } + case api.ProviderGCP: + if gcs, err := askGCPCloudProviderSettings(); err != nil { + return nil, err + } else { + return &gqlclient.CloudProviderSettingsAttributes{Gcp: gcs}, nil + } + } + + return nil, fmt.Errorf("unknown provider") +} + +func askAWSCloudProviderSettings() (*gqlclient.AwsSettingsAttributes, error) { + awsSurvey := []*survey.Question{ + { + Name: "key", + Prompt: &survey.Input{Message: "Enter the Access Key ID:"}, + }, + { + Name: "secret", + Prompt: &survey.Input{Message: "Enter Secret Access Key:"}, + }, + } + var resp struct { + Key string + Secret string + } + if err := survey.Ask(awsSurvey, &resp); err != nil { + return nil, err + } + return &gqlclient.AwsSettingsAttributes{ + AccessKeyID: resp.Key, + SecretAccessKey: resp.Secret, + }, nil +} + +func askAzureCloudProviderSettings() (*gqlclient.AzureSettingsAttributes, error) { + azureSurvey := []*survey.Question{ + { + Name: "tenant", + Prompt: &survey.Input{Message: "Enter the tenant ID:"}, + }, + { + Name: "client", + Prompt: &survey.Input{Message: "Enter the client ID:"}, + }, + { + Name: "secret", + Prompt: &survey.Input{Message: "Enter the client secret:"}, + }, + { + Name: "subscription", + Prompt: &survey.Input{Message: "Enter the subscription ID:"}, + }, + } + var resp struct { + Tenant string + Client string + Secret string + Subscription string + } + if err := survey.Ask(azureSurvey, &resp); err != nil { + return nil, err + } + return &gqlclient.AzureSettingsAttributes{ + TenantID: resp.Tenant, + ClientID: resp.Client, + ClientSecret: resp.Secret, + SubscriptionID: resp.Subscription, + }, nil +} + +func askGCPCloudProviderSettings() (*gqlclient.GcpSettingsAttributes, error) { + applicationCredentialsFilePath := "" + + prompt := &survey.Input{ + Message: "Enter GCP application credentials file path:", + } + if err := survey.AskOne(prompt, &applicationCredentialsFilePath, survey.WithValidator(validServiceAccountCredentials)); err != nil { + return nil, err + } + + return &gqlclient.GcpSettingsAttributes{ + ApplicationCredentials: toCredentialsJSON(applicationCredentialsFilePath), + }, nil +} + +type credentials struct { + Email string `json:"client_email"` + ID string `json:"client_id"` + Type credentialsType `json:"type"` +} + +type credentialsType = string + +const ( + ServiceAccountType credentialsType = "service_account" +) + +func validServiceAccountCredentials(val interface{}) error { + path, _ := val.(string) + bytes, err := os.ReadFile(path) + if err != nil { + return err + } + + creds := new(credentials) + if err = json.Unmarshal(bytes, creds); err != nil { + return err + } + + if creds.Type != ServiceAccountType || len(creds.Email) == 0 || len(creds.ID) == 0 { + return fmt.Errorf("provided credentials file is not a valid service account. Must have type 'service_account' and both 'client_id' and 'client_email' set") + } + + return nil +} + +func toCredentialsJSON(path string) string { + bytes, _ := os.ReadFile(path) + return string(bytes) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index a933a362..5ad5e322 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -33,6 +33,7 @@ type Metadata struct { type Config struct { Email string `json:"email"` Token string `yaml:"token" json:"token"` + ConsoleToken string `yaml:"consoleToken" json:"consoleToken"` NamespacePrefix string `yaml:"namespacePrefix"` Endpoint string `yaml:"endpoint"` LockProfile string `yaml:"lockProfile"` @@ -98,7 +99,10 @@ func Profiles() ([]*VersionedConfig, error) { if err = yaml.Unmarshal(contents, versioned); err != nil { return nil, err } - confs = append(confs, versioned) + + if versioned.Kind == "Config" { + confs = append(confs, versioned) + } } } diff --git a/pkg/console/agent.go b/pkg/console/agent.go new file mode 100644 index 00000000..0eba65f1 --- /dev/null +++ b/pkg/console/agent.go @@ -0,0 +1,73 @@ +package console + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/pluralsh/plural/pkg/helm" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/storage/driver" +) + +const ( + releaseName = "deploy-operator" + repoUrl = "https://pluralsh.github.io/deployment-operator" +) + +func InstallAgent(url, token, namespace string) error { + settings := cli.New() + vals := map[string]interface{}{ + "secrets": map[string]string{ + "deployToken": token, + }, + "consoleUrl": url, + } + + if err := helm.AddRepo(releaseName, repoUrl); err != nil { + return err + } + + helmConfig, err := helm.GetActionConfig(namespace) + if err != nil { + return err + } + + cp, err := action.NewInstall(helmConfig).ChartPathOptions.LocateChart(fmt.Sprintf("%s/%s", releaseName, "deployment-operator"), settings) + if err != nil { + return err + } + + chart, err := loader.Load(cp) + if err != nil { + return err + } + + histClient := action.NewHistory(helmConfig) + histClient.Max = 5 + + if _, err = histClient.Run(releaseName); errors.Is(err, driver.ErrReleaseNotFound) { + fmt.Println("installing deployment operator...") + instClient := action.NewInstall(helmConfig) + instClient.Namespace = namespace + instClient.ReleaseName = releaseName + instClient.Timeout = time.Minute * 5 + _, err = instClient.Run(chart, vals) + if err != nil { + return err + } + return nil + } + fmt.Println("upgrading deployment operator...") + client := action.NewUpgrade(helmConfig) + client.Namespace = namespace + client.Timeout = time.Minute * 5 + _, err = client.Run(releaseName, chart, vals) + return err +} + +func UninstallAgent(namespace string) error { + return helm.Uninstall(releaseName, namespace) +} diff --git a/pkg/console/clusters.go b/pkg/console/clusters.go new file mode 100644 index 00000000..8532d3c4 --- /dev/null +++ b/pkg/console/clusters.go @@ -0,0 +1,60 @@ +package console + +import ( + "fmt" + + consoleclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/api" +) + +func (c *consoleClient) ListClusters() (*consoleclient.ListClusters, error) { + + result, err := c.client.ListClusters(c.ctx, nil, nil, nil) + if err != nil { + return nil, api.GetErrorResponse(err, "ListClusters") + } + + return result, nil +} + +func (c *consoleClient) GetCluster(clusterId, clusterName *string) (*consoleclient.ClusterFragment, error) { + if clusterId == nil && clusterName == nil { + return nil, fmt.Errorf("clusterId and clusterName can not be null") + } + if clusterId != nil { + result, err := c.client.GetCluster(c.ctx, clusterId) + if err != nil { + return nil, api.GetErrorResponse(err, "GetCluster") + } + return result.Cluster, nil + } + result, err := c.client.GetClusterByHandle(c.ctx, clusterName) + if err != nil { + return nil, api.GetErrorResponse(err, "GetCluster") + } + + return result.Cluster, nil +} + +func (c *consoleClient) UpdateCluster(id string, attr consoleclient.ClusterUpdateAttributes) (*consoleclient.UpdateCluster, error) { + + result, err := c.client.UpdateCluster(c.ctx, id, attr) + if err != nil { + return nil, api.GetErrorResponse(err, "UpdateCluster") + } + + return result, nil +} + +func (c *consoleClient) DeleteCluster(id string) error { + _, err := c.client.DeleteCluster(c.ctx, id) + return api.GetErrorResponse(err, "DeleteCluster") +} + +func (c *consoleClient) CreateCluster(attributes consoleclient.ClusterAttributes) (*consoleclient.CreateCluster, error) { + newCluster, err := c.client.CreateCluster(c.ctx, attributes) + if err != nil { + return nil, api.GetErrorResponse(err, "CreateCluster") + } + return newCluster, nil +} diff --git a/pkg/console/config.go b/pkg/console/config.go new file mode 100644 index 00000000..64ec0750 --- /dev/null +++ b/pkg/console/config.go @@ -0,0 +1,61 @@ +package console + +import ( + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +const ( + pluralDir = ".plural" + ConfigName = "console.yml" +) + +type VersionedConfig struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Spec *Config `yaml:"spec"` +} + +type Config struct { + Url string `yaml:"url"` + Token string `yaml:"token"` +} + +func configFile() string { + folder, _ := os.UserHomeDir() + return filepath.Join(folder, pluralDir, ConfigName) +} + +func ReadConfig() (conf Config) { + contents, err := os.ReadFile(configFile()) + if err != nil { + return + } + + versioned := &VersionedConfig{Spec: &conf} + if err = yaml.Unmarshal(contents, versioned); err != nil { + return Config{} + } + return +} + +func (conf *Config) Save() error { + versioned := &VersionedConfig{ + ApiVersion: "platform.plural.sh/v1alpha1", + Kind: "Console", + Spec: conf, + } + io, err := yaml.Marshal(versioned) + if err != nil { + return err + } + + f := configFile() + if err := os.MkdirAll(filepath.Dir(f), os.ModePerm); err != nil { + return err + } + + return os.WriteFile(f, io, 0644) +} diff --git a/pkg/console/console.go b/pkg/console/console.go new file mode 100644 index 00000000..5227f826 --- /dev/null +++ b/pkg/console/console.go @@ -0,0 +1,59 @@ +package console + +import ( + "context" + "fmt" + "net/http" + + consoleclient "github.com/pluralsh/console-client-go" +) + +type consoleClient struct { + ctx context.Context + client *consoleclient.Client + url string + token string +} + +type ConsoleClient interface { + Url() string + Token() string + ListClusters() (*consoleclient.ListClusters, error) + GetCluster(clusterId, clusterName *string) (*consoleclient.ClusterFragment, error) + UpdateCluster(id string, attr consoleclient.ClusterUpdateAttributes) (*consoleclient.UpdateCluster, error) + DeleteCluster(id string) error + ListClusterServices(clusterId, handle *string) ([]*consoleclient.ServiceDeploymentEdgeFragment, error) + CreateRepository(url string, privateKey, passphrase, username, password *string) (*consoleclient.CreateGitRepository, error) + ListRepositories() (*consoleclient.ListGitRepositories, error) + UpdateRepository(id string, attrs consoleclient.GitAttributes) (*consoleclient.UpdateGitRepository, error) + CreateClusterService(clusterId, clusterName *string, attr consoleclient.ServiceDeploymentAttributes) (*consoleclient.ServiceDeploymentFragment, error) + UpdateClusterService(serviceId, serviceName, clusterName *string, attributes consoleclient.ServiceUpdateAttributes) (*consoleclient.ServiceDeploymentFragment, error) + CloneService(clusterId string, serviceId, serviceName, clusterName *string, attributes consoleclient.ServiceCloneAttributes) (*consoleclient.ServiceDeploymentFragment, error) + GetClusterService(serviceId, serviceName, clusterName *string) (*consoleclient.ServiceDeploymentExtended, error) + DeleteClusterService(serviceId string) (*consoleclient.DeleteServiceDeployment, error) + ListProviders() (*consoleclient.ListProviders, error) + CreateProviderCredentials(name string, attr consoleclient.ProviderCredentialAttributes) (*consoleclient.CreateProviderCredential, error) + DeleteProviderCredentials(id string) (*consoleclient.DeleteProviderCredential, error) + SavePipeline(name string, attrs consoleclient.PipelineAttributes) (*consoleclient.PipelineFragment, error) + CreateCluster(attributes consoleclient.ClusterAttributes) (*consoleclient.CreateCluster, error) + CreateProvider(attr consoleclient.ClusterProviderAttributes) (*consoleclient.CreateClusterProvider, error) +} + +func NewConsoleClient(token, url string) (ConsoleClient, error) { + return &consoleClient{ + url: url, + token: token, + client: consoleclient.NewClient(http.DefaultClient, fmt.Sprintf("%s/gql", url), func(req *http.Request) { + req.Header.Set("Authorization", fmt.Sprintf("Token %s", token)) + }), + ctx: context.Background(), + }, nil +} + +func (client *consoleClient) Url() string { + return client.url +} + +func (client *consoleClient) Token() string { + return client.token +} diff --git a/pkg/console/describe.go b/pkg/console/describe.go new file mode 100644 index 00000000..3e68b39e --- /dev/null +++ b/pkg/console/describe.go @@ -0,0 +1,263 @@ +package console + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" + "text/tabwriter" + + consoleclient "github.com/pluralsh/console-client-go" + "k8s.io/cli-runtime/pkg/printers" +) + +// Each level has 2 spaces for PrefixWriter +const ( + LEVEL_0 = iota + LEVEL_1 + LEVEL_2 + LEVEL_3 + LEVEL_4 +) + +func DescribeCluster(cluster *consoleclient.ClusterFragment) (string, error) { + return tabbedString(func(out io.Writer) error { + w := NewPrefixWriter(out) + w.Write(LEVEL_0, "Id:\t%s\n", cluster.ID) + w.Write(LEVEL_0, "Name:\t%s\n", cluster.Name) + if cluster.Handle != nil { + w.Write(LEVEL_0, "Handle:\t@%s\n", *cluster.Handle) + } + if cluster.Version != nil { + w.Write(LEVEL_0, "Version:\t%s\n", *cluster.Version) + } + if cluster.CurrentVersion != nil { + w.Write(LEVEL_0, "Current Version:\t%s\n", *cluster.CurrentVersion) + } + if cluster.PingedAt != nil { + w.Write(LEVEL_0, "Pinged At:\t%s\n", *cluster.PingedAt) + } + if cluster.Self != nil { + w.Write(LEVEL_0, "Self:\t%v\n", *cluster.Self) + } + + if cluster.Provider != nil { + w.Write(LEVEL_0, "Provider:\n") + w.Write(LEVEL_1, "Id:\t%s\n", cluster.Provider.ID) + w.Write(LEVEL_1, "Name:\t%s\n", cluster.Provider.Name) + w.Write(LEVEL_1, "Namespace:\t%s\n", cluster.Provider.Namespace) + w.Write(LEVEL_1, "Editable:\t%v\n", *cluster.Provider.Editable) + w.Write(LEVEL_1, "Cloud:\t%v\n", cluster.Provider.Cloud) + if cluster.Provider.Repository != nil { + w.Write(LEVEL_1, "Git:\n") + w.Write(LEVEL_2, "Id:\t%s\n", cluster.Provider.Repository.ID) + w.Write(LEVEL_2, "Url:\t%s\n", cluster.Provider.Repository.URL) + if cluster.Provider.Repository.AuthMethod != nil { + w.Write(LEVEL_2, "Auth Method:\t%v\n", *cluster.Provider.Repository.AuthMethod) + } + if cluster.Provider.Repository.Health != nil { + w.Write(LEVEL_2, "Health:\t%v\n", *cluster.Provider.Repository.Health) + } + if cluster.Provider.Repository.Error != nil { + w.Write(LEVEL_2, "Error:\t%s\n", *cluster.Provider.Repository.Error) + } + } + } + + return nil + }) +} + +func DescribeService(service *consoleclient.ServiceDeploymentExtended) (string, error) { + return tabbedString(func(out io.Writer) error { + w := NewPrefixWriter(out) + w.Write(LEVEL_0, "Id:\t%s\n", service.ID) + w.Write(LEVEL_0, "Name:\t%s\n", service.Name) + w.Write(LEVEL_0, "Namespace:\t%s\n", service.Namespace) + w.Write(LEVEL_0, "Version:\t%s\n", service.Version) + if service.Tarball != nil { + w.Write(LEVEL_0, "Tarball:\t%s\n", *service.Tarball) + } else { + w.Write(LEVEL_0, "Tarball:\t%s\n", "") + } + if service.DeletedAt != nil { + w.Write(LEVEL_0, "Status:\tTerminating (lasts %s)\n", *service.DeletedAt) + } + w.Write(LEVEL_0, "Git:\t\n") + w.Write(LEVEL_1, "Ref:\t%s\n", service.Git.Ref) + w.Write(LEVEL_1, "Folder:\t%s\n", service.Git.Folder) + if service.Revision != nil { + w.Write(LEVEL_1, "Revision:\t\n") + w.Write(LEVEL_2, "Id:\t%s\n", service.Revision.ID) + + } + if service.Kustomize != nil { + w.Write(LEVEL_0, "Kustomize:\t\n") + w.Write(LEVEL_1, "Path:\t%s\n", service.Kustomize.Path) + } + if service.Repository != nil { + w.Write(LEVEL_0, "Repository:\t\n") + w.Write(LEVEL_1, "Id:\t%s\n", service.Repository.ID) + w.Write(LEVEL_1, "Url:\t%s\n", service.Repository.URL) + if service.Repository.AuthMethod != nil { + w.Write(LEVEL_1, "AuthMethod:\t%s\n", *service.Repository.AuthMethod) + } + w.Write(LEVEL_1, "Status:\t\n") + if service.Repository.Health != nil { + w.Write(LEVEL_2, "Health:\t%s\n", *service.Repository.Health) + } + if service.Repository.Error != nil { + w.Write(LEVEL_2, "Error:\t%s\n", *service.Repository.Error) + } + configMap := map[string]string{} + for _, conf := range service.Configuration { + configMap[conf.Name] = conf.Value + } + printConfigMultiline(w, "Configuration", configMap) + if len(service.Components) > 0 { + w.Write(LEVEL_0, "Components:\n Id\tName\tNamespace\tKind\tVersion\tState\tSynced\n") + w.Write(LEVEL_1, "----\t------\t------\t------\t------\t------\t------\n") + for _, c := range service.Components { + namespace := "-" + version := "-" + state := "-" + if c.Namespace != nil { + namespace = *c.Namespace + } + if c.Version != nil { + version = *c.Version + } + if c.State != nil { + state = string(*c.State) + } + + w.Write(LEVEL_1, "%v \t%v\t%v\t%v\t%v\t%v\t%v\n", c.ID, c.Name, namespace, c.Kind, version, state, c.Synced) + } + } else { + w.Write(LEVEL_0, "Components: %s\n", "") + } + if len(service.Errors) > 0 { + w.Write(LEVEL_0, "Errors:\n Source\tMessage\n") + w.Write(LEVEL_1, "----\t------\n") + for _, c := range service.Errors { + + w.Write(LEVEL_1, "%v \t%v\n", c.Source, c.Message) + } + } else { + w.Write(LEVEL_0, "Errors: %s\n", "") + } + + } + return nil + }) +} + +var maxConfigLen = 140 + +func printConfigMultiline(w PrefixWriter, title string, configurations map[string]string) { + w.Write(LEVEL_0, "%s:\t", title) + + // to print labels in the sorted order + keys := make([]string, 0, len(configurations)) + for key := range configurations { + keys = append(keys, key) + } + if len(keys) == 0 { + w.WriteLine("") + return + } + sort.Strings(keys) + indent := "\t" + for i, key := range keys { + if i != 0 { + w.Write(LEVEL_0, indent) + } + value := strings.TrimSuffix(configurations[key], "\n") + if (len(value)+len(key)+2) > maxConfigLen || strings.Contains(value, "\n") { + w.Write(LEVEL_0, "%s:\n", key) + for _, s := range strings.Split(value, "\n") { + w.Write(LEVEL_0, "%s %s\n", indent, shorten(s, maxConfigLen-2)) + } + } else { + w.Write(LEVEL_0, "%s: %s\n", key, value) + } + } +} + +func shorten(s string, maxLength int) string { + if len(s) > maxLength { + return s[:maxLength] + "..." + } + return s +} + +func tabbedString(f func(io.Writer) error) (string, error) { + out := new(tabwriter.Writer) + buf := &bytes.Buffer{} + out.Init(buf, 0, 8, 2, ' ', 0) + + err := f(out) + if err != nil { + return "", err + } + + err = out.Flush() + if err != nil { + return "", err + } + return buf.String(), nil +} + +type flusher interface { + Flush() +} + +// PrefixWriter can write text at various indentation levels. +type PrefixWriter interface { + // Write writes text with the specified indentation level. + Write(level int, format string, a ...interface{}) + // WriteLine writes an entire line with no indentation level. + WriteLine(a ...interface{}) + // Flush forces indentation to be reset. + Flush() +} + +// prefixWriter implements PrefixWriter +type prefixWriter struct { + out io.Writer +} + +var _ PrefixWriter = &prefixWriter{} + +// NewPrefixWriter creates a new PrefixWriter. +func NewPrefixWriter(out io.Writer) PrefixWriter { + return &prefixWriter{out: out} +} + +func (pw *prefixWriter) Write(level int, format string, a ...interface{}) { + levelSpace := " " + prefix := "" + for i := 0; i < level; i++ { + prefix += levelSpace + } + output := fmt.Sprintf(prefix+format, a...) + err := printers.WriteEscaped(pw.out, output) + if err != nil { + return + } +} + +func (pw *prefixWriter) WriteLine(a ...interface{}) { + output := fmt.Sprintln(a...) + err := printers.WriteEscaped(pw.out, output) + if err != nil { + return + } +} + +func (pw *prefixWriter) Flush() { + if f, ok := pw.out.(flusher); ok { + f.Flush() + } +} diff --git a/pkg/console/pipelines.go b/pkg/console/pipelines.go new file mode 100644 index 00000000..1ff38bef --- /dev/null +++ b/pkg/console/pipelines.go @@ -0,0 +1,94 @@ +package console + +import ( + "strings" + + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/api" + "github.com/pluralsh/plural/pkg/utils" + "github.com/pluralsh/polly/algorithms" + "github.com/samber/lo" + "sigs.k8s.io/yaml" +) + +type Pipeline struct { + Name string `json:"name"` + Stages []PipelineStage `json:"stages"` + Edges []PipelineEdge `json:"edges"` +} + +type PipelineStage struct { + Name string `json:"name"` + Services []StageService `json:"services"` +} + +type StageService struct { + Name string `json:"name"` + Criteria *PromotionCriteria `json:"criteria"` +} + +type PromotionCriteria struct { + Source string `json:"source"` + Secrets []string `json:"secrets"` +} + +type PipelineEdge struct { + From string `json:"from"` + To string `json:"to"` +} + +func (c *consoleClient) SavePipeline(name string, attrs gqlclient.PipelineAttributes) (*gqlclient.PipelineFragment, error) { + result, err := c.client.SavePipeline(c.ctx, name, attrs) + if err != nil { + return nil, api.GetErrorResponse(err, "SavePipeline") + } + + return result.SavePipeline, nil +} + +func ConstructPipelineInput(input []byte) (string, *gqlclient.PipelineAttributes, error) { + var pipe Pipeline + if err := yaml.Unmarshal(input, &pipe); err != nil { + return "", nil, err + } + pipeline := &gqlclient.PipelineAttributes{} + pipeline.Edges = algorithms.Map(pipe.Edges, func(e PipelineEdge) *gqlclient.PipelineEdgeAttributes { + return &gqlclient.PipelineEdgeAttributes{From: lo.ToPtr(e.From), To: lo.ToPtr(e.To)} + }) + pipeline.Stages = algorithms.Map(pipe.Stages, func(s PipelineStage) *gqlclient.PipelineStageAttributes { + stage := &gqlclient.PipelineStageAttributes{Name: s.Name} + stage.Services = algorithms.Map(s.Services, func(s StageService) *gqlclient.StageServiceAttributes { + handle, name := handleName(s.Name) + return &gqlclient.StageServiceAttributes{ + Handle: lo.ToPtr(handle), + Name: lo.ToPtr(name), + Criteria: buildCriteria(s.Criteria), + } + }) + return stage + }) + return pipe.Name, pipeline, nil +} + +func buildCriteria(criteria *PromotionCriteria) *gqlclient.PromotionCriteriaAttributes { + if criteria == nil { + return nil + } + + handle, name := handleName(criteria.Source) + return &gqlclient.PromotionCriteriaAttributes{ + Handle: lo.ToPtr(handle), + Name: lo.ToPtr(name), + Secrets: lo.ToSlicePtr(criteria.Secrets), + } +} + +func handleName(name string) (string, string) { + parts := strings.Split(name, "/") + if len(parts) == 2 { + return parts[0], parts[1] + } + + utils.Error("invalid name: %s, should be of the format {handle}/{name}\n", name) + return "", "" +} diff --git a/pkg/console/providers.go b/pkg/console/providers.go new file mode 100644 index 00000000..5ef51f14 --- /dev/null +++ b/pkg/console/providers.go @@ -0,0 +1,45 @@ +package console + +import ( + consoleclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/api" +) + +func (c *consoleClient) ListProviders() (*consoleclient.ListProviders, error) { + + result, err := c.client.ListProviders(c.ctx) + if err != nil { + return nil, api.GetErrorResponse(err, "ListProviders") + } + + return result, nil +} + +func (c *consoleClient) CreateProviderCredentials(name string, attr consoleclient.ProviderCredentialAttributes) (*consoleclient.CreateProviderCredential, error) { + + result, err := c.client.CreateProviderCredential(c.ctx, attr, name) + if err != nil { + return nil, api.GetErrorResponse(err, "CreateProviderCredential") + } + + return result, nil +} + +func (c *consoleClient) DeleteProviderCredentials(id string) (*consoleclient.DeleteProviderCredential, error) { + + result, err := c.client.DeleteProviderCredential(c.ctx, id) + if err != nil { + return nil, api.GetErrorResponse(err, "DeleteProviderCredential") + } + + return result, nil +} + +func (c *consoleClient) CreateProvider(attr consoleclient.ClusterProviderAttributes) (*consoleclient.CreateClusterProvider, error) { + result, err := c.client.CreateClusterProvider(c.ctx, attr) + if err != nil { + return nil, api.GetErrorResponse(err, "CreateProvider") + } + + return result, nil +} diff --git a/pkg/console/repositories.go b/pkg/console/repositories.go new file mode 100644 index 00000000..a40398b5 --- /dev/null +++ b/pkg/console/repositories.go @@ -0,0 +1,39 @@ +package console + +import ( + gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural/pkg/api" +) + +func (c *consoleClient) CreateRepository(url string, privateKey, passphrase, username, password *string) (*gqlclient.CreateGitRepository, error) { + attrs := gqlclient.GitAttributes{ + URL: url, + PrivateKey: privateKey, + Passphrase: passphrase, + Username: username, + Password: password, + } + res, err := c.client.CreateGitRepository(c.ctx, attrs) + if err != nil { + return nil, api.GetErrorResponse(err, "CreateGitRepository") + } + return res, nil +} + +func (c *consoleClient) ListRepositories() (*gqlclient.ListGitRepositories, error) { + result, err := c.client.ListGitRepositories(c.ctx, nil, nil, nil) + if err != nil { + return nil, api.GetErrorResponse(err, "ListRepositories") + } + + return result, nil +} + +func (c *consoleClient) UpdateRepository(id string, attrs gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error) { + + res, err := c.client.UpdateGitRepository(c.ctx, id, attrs) + if err != nil { + return nil, api.GetErrorResponse(err, "UpdateGitRepository") + } + return res, nil +} diff --git a/pkg/console/services.go b/pkg/console/services.go new file mode 100644 index 00000000..42e2b9f2 --- /dev/null +++ b/pkg/console/services.go @@ -0,0 +1,138 @@ +package console + +import ( + "fmt" + + gqlclient "github.com/pluralsh/console-client-go" + + "github.com/pluralsh/plural/pkg/api" +) + +func (c *consoleClient) ListClusterServices(clusterId, clusterName *string) ([]*gqlclient.ServiceDeploymentEdgeFragment, error) { + if clusterId == nil && clusterName == nil { + return nil, fmt.Errorf("clusterId and clusterName can not be null") + } + if clusterId != nil { + result, err := c.client.ListServiceDeployment(c.ctx, nil, nil, nil, clusterId) + if err != nil { + return nil, api.GetErrorResponse(err, "ListServiceDeployment") + } + if result == nil { + return nil, fmt.Errorf("the result from ListServiceDeployment is null") + } + return result.ServiceDeployments.Edges, nil + } + result, err := c.client.ListServiceDeploymentByHandle(c.ctx, nil, nil, nil, clusterName) + if err != nil { + return nil, api.GetErrorResponse(err, "ListServiceDeploymentByHandle") + } + if result == nil { + return nil, fmt.Errorf("the result from ListServiceDeploymentByHandle is null") + } + return result.ServiceDeployments.Edges, nil +} + +func (c *consoleClient) CreateClusterService(clusterId, clusterName *string, attributes gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error) { + if clusterId == nil && clusterName == nil { + return nil, fmt.Errorf("clusterId and clusterName can not be null") + } + if clusterId != nil { + result, err := c.client.CreateServiceDeployment(c.ctx, *clusterId, attributes) + if err != nil { + return nil, api.GetErrorResponse(err, "CreateServiceDeployment") + } + if result == nil { + return nil, fmt.Errorf("the result from CreateServiceDeployment is null") + } + return result.CreateServiceDeployment, nil + } + + result, err := c.client.CreateServiceDeploymentWithHandle(c.ctx, *clusterName, attributes) + if err != nil { + return nil, api.GetErrorResponse(err, "CreateServiceDeploymentWithHandle") + } + if result == nil { + return nil, fmt.Errorf("the result from CreateServiceDeploymentWithHandle is null") + } + return result.CreateServiceDeployment, nil +} + +func (c *consoleClient) UpdateClusterService(serviceId, serviceName, clusterName *string, attributes gqlclient.ServiceUpdateAttributes) (*gqlclient.ServiceDeploymentFragment, error) { + if serviceId == nil && serviceName == nil && clusterName == nil { + return nil, fmt.Errorf("serviceId, serviceName and clusterName can not be null") + } + if serviceId != nil { + result, err := c.client.UpdateServiceDeployment(c.ctx, *serviceId, attributes) + if err != nil { + return nil, api.GetErrorResponse(err, "UpdateClusterService") + } + if result == nil { + return nil, fmt.Errorf("the result from UpdateServiceDeployment is null") + } + + return result.UpdateServiceDeployment, nil + } + result, err := c.client.UpdateServiceDeploymentWithHandle(c.ctx, *clusterName, *serviceName, attributes) + if err != nil { + return nil, api.GetErrorResponse(err, "UpdateServiceDeploymentWithHandle") + } + if result == nil { + return nil, fmt.Errorf("the result from UpdateServiceDeploymentWithHandle is null") + } + + return result.UpdateServiceDeployment, nil +} + +func (c *consoleClient) CloneService(clusterId string, serviceId, serviceName, clusterName *string, attributes gqlclient.ServiceCloneAttributes) (*gqlclient.ServiceDeploymentFragment, error) { + if serviceId == nil && serviceName == nil && clusterName == nil { + return nil, fmt.Errorf("serviceId, serviceName and clusterName can not be null") + } + if serviceId != nil { + result, err := c.client.CloneServiceDeployment(c.ctx, clusterId, *serviceId, attributes) + if err != nil { + return nil, api.GetErrorResponse(err, "CloneService") + } + + return result.CloneService, nil + } + result, err := c.client.CloneServiceDeploymentWithHandle(c.ctx, clusterId, *clusterName, *serviceName, attributes) + if err != nil { + return nil, api.GetErrorResponse(err, "CloneServiceWithHandle") + } + + return result.CloneService, nil +} + +func (c *consoleClient) GetClusterService(serviceId, serviceName, clusterName *string) (*gqlclient.ServiceDeploymentExtended, error) { + if serviceId == nil && serviceName == nil && clusterName == nil { + return nil, fmt.Errorf("serviceId, serviceName and clusterName can not be null") + } + if serviceId != nil { + result, err := c.client.GetServiceDeployment(c.ctx, *serviceId) + if err != nil { + return nil, api.GetErrorResponse(err, "GetClusterService") + } + if result == nil { + return nil, fmt.Errorf("the result from GetServiceDeployment is null") + } + return result.ServiceDeployment, nil + } + result, err := c.client.GetServiceDeploymentByHandle(c.ctx, *clusterName, *serviceName) + if err != nil { + return nil, api.GetErrorResponse(err, "GetServiceDeploymentByHandle") + } + if result == nil { + return nil, fmt.Errorf("the result from GetServiceDeploymentByHandle is null") + } + + return result.ServiceDeployment, nil +} + +func (c *consoleClient) DeleteClusterService(serviceId string) (*gqlclient.DeleteServiceDeployment, error) { + result, err := c.client.DeleteServiceDeployment(c.ctx, serviceId) + if err != nil { + return nil, api.GetErrorResponse(err, "DeleteClusterService") + } + + return result, nil +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 12932b14..50c2247d 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -2,16 +2,27 @@ package helm import ( "bytes" + "context" "fmt" "log" + "os" + "path/filepath" "strings" + "time" + "github.com/gofrs/flock" "github.com/pluralsh/plural/pkg/utils" + "gopkg.in/yaml.v2" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/repo" ) +var settings = cli.New() + func debug(format string, v ...interface{}) { if utils.EnableDebug { format = fmt.Sprintf("INFO: %s\n", format) @@ -25,6 +36,10 @@ func debug(format string, v ...interface{}) { func GetActionConfig(namespace string) (*action.Configuration, error) { actionConfig := new(action.Configuration) settings := cli.New() + if os.Getenv("KUBECONFIG") != "" { + settings.KubeConfig = os.Getenv("KUBECONFIG") + } + settings.SetNamespace(namespace) settings.Debug = false if err := actionConfig.Init(settings.RESTClientGetter(), namespace, "", debug); err != nil { @@ -75,3 +90,110 @@ func Lint(path, namespace string, values map[string]interface{}) error { } return nil } + +func AddRepo(repoName, repoUrl string) error { + repoFile := getEnvVar("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")) + err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + // Acquire a file lock for process synchronization. + repoFileExt := filepath.Ext(repoFile) + var lockPath string + if len(repoFileExt) > 0 && len(repoFileExt) < len(repoFile) { + lockPath = strings.TrimSuffix(repoFile, repoFileExt) + ".lock" + } else { + lockPath = repoFile + ".lock" + } + fileLock := flock.New(lockPath) + lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + locked, err := fileLock.TryLockContext(lockCtx, time.Second) + if err == nil && locked { + defer func(fileLock *flock.Flock) { + _ = fileLock.Unlock() + }(fileLock) + } + if err != nil { + return err + } + + b, err := os.ReadFile(repoFile) + if err != nil && !os.IsNotExist(err) { + return err + } + + var f repo.File + if err := yaml.Unmarshal(b, &f); err != nil { + return err + } + + c := repo.Entry{ + Name: repoName, + URL: repoUrl, + InsecureSkipTLSverify: true, + } + + // If the repo exists do one of two things: + // 1. If the configuration for the name is the same continue without error. + // 2. When the config is different require --force-update. + + // always force updates for now + // if f.Has(repoName) { + // return nil + // } + + r, err := repo.NewChartRepository(&c, getter.All(settings)) + if err != nil { + return err + } + + if _, err := r.DownloadIndexFile(); err != nil { + return fmt.Errorf("looks like %q is not a valid chart repository or cannot be reached", repoUrl) + } + + f.Update(&c) + return f.WriteFile(repoFile, 0644) +} + +func Uninstall(name, namespace string) error { + if available, err := IsReleaseAvailable(name, namespace); !available { + return err + } + + actionConfig, err := GetActionConfig(namespace) + if err != nil { + return err + } + client := action.NewUninstall(actionConfig) + + _, err = client.Run(name) + return err +} + +func IsReleaseAvailable(name, namespace string) (bool, error) { + actionConfig, err := GetActionConfig(namespace) + if err != nil { + return false, err + } + client := action.NewList(actionConfig) + resp, err := client.Run() + if err != nil { + return false, err + } + for _, rel := range resp { + if rel.Name == name { + return true, nil + } + } + return false, nil +} + +func getEnvVar(name, defaultValue string) string { + if v, ok := os.LookupEnv(name); ok { + return v + } + + return defaultValue +} diff --git a/pkg/kubernetes/config/kubeconfig.go b/pkg/kubernetes/config/kubeconfig.go new file mode 100644 index 00000000..e3e58115 --- /dev/null +++ b/pkg/kubernetes/config/kubeconfig.go @@ -0,0 +1,47 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pluralsh/plural/pkg/utils" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func GetKubeconfig(path, context string) (string, error) { + if strings.HasPrefix(path, "~/") { + home, _ := os.UserHomeDir() + path = filepath.Join(home, path[2:]) + } + if !utils.Exists(path) { + return "", fmt.Errorf("the specified path does not exist") + } + + config, err := clientcmd.LoadFromFile(path) + if err != nil { + return "", err + } + + if context != "" { + if config.Contexts[context] == nil { + return "", fmt.Errorf("the given context doesn't exist") + } + config.CurrentContext = context + } + newConfig := *clientcmdapi.NewConfig() + newConfig.CurrentContext = config.CurrentContext + newConfig.Contexts[config.CurrentContext] = config.Contexts[config.CurrentContext] + newConfig.Clusters[config.CurrentContext] = config.Clusters[config.CurrentContext] + newConfig.AuthInfos[config.CurrentContext] = config.AuthInfos[config.CurrentContext] + newConfig.Extensions[config.CurrentContext] = config.Extensions[config.CurrentContext] + newConfig.Preferences = config.Preferences + result, err := clientcmd.Write(newConfig) + if err != nil { + return "", err + } + + return string(result), nil +} diff --git a/pkg/kubernetes/kube.go b/pkg/kubernetes/kube.go index 65ea2a39..2fb513b7 100644 --- a/pkg/kubernetes/kube.go +++ b/pkg/kubernetes/kube.go @@ -50,6 +50,7 @@ type Kube interface { Secret(namespace string, name string) (*v1.Secret, error) SecretList(namespace string, opts metav1.ListOptions) (*v1.SecretList, error) SecretCreate(namespace string, secret *v1.Secret) (*v1.Secret, error) + SecretDelete(namespace string, secretName string) error SecretDeleteCollection(namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error Node(name string) (*v1.Node, error) Nodes() (*v1.NodeList, error) @@ -91,6 +92,15 @@ func KubeConfig() (*rest.Config, error) { } func KubeConfigWithContext(context string) (*rest.Config, error) { + if os.Getenv("KUBECONFIG") != "" { + conf := os.Getenv("KUBECONFIG") + if len(context) > 0 { + return buildConfigFromFlags(context, conf) + } + + return clientcmd.BuildConfigFromFlags("", conf) + } + if InKubernetes() { return rest.InClusterConfig() } @@ -186,6 +196,10 @@ func (k *kube) SecretCreate(namespace string, secret *v1.Secret) (*v1.Secret, er return k.Kube.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{}) } +func (k *kube) SecretDelete(namespace string, secretName string) error { + return k.Kube.CoreV1().Secrets(namespace).Delete(context.Background(), secretName, metav1.DeleteOptions{}) +} + func (k *kube) SecretDeleteCollection(namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { return k.Kube.CoreV1().Secrets(namespace).DeleteCollection(context.Background(), opts, listOpts) } diff --git a/pkg/scaffold/template/lua_test.go b/pkg/scaffold/template/lua_test.go index 4f6218af..d26172ac 100644 --- a/pkg/scaffold/template/lua_test.go +++ b/pkg/scaffold/template/lua_test.go @@ -84,6 +84,7 @@ test: spec: email: test@plural.sh token: abc + consoleToken: "" namespacePrefix: test endpoint: http://example.com lockProfile: abc @@ -156,6 +157,7 @@ test: spec: email: test@plural.sh token: abc + consoleToken: "" namespacePrefix: test endpoint: http://example.com lockProfile: abc diff --git a/pkg/server/setup.go b/pkg/server/setup.go index 0f799943..29c96f6a 100644 --- a/pkg/server/setup.go +++ b/pkg/server/setup.go @@ -53,6 +53,7 @@ func toContext(setup *SetupRequest) *manifest.Context { "passphrase": "", "repo_url": setup.GitUrl, "console_dns": fmt.Sprintf("console.%s", setup.Workspace.Subdomain), + "kas_dns": fmt.Sprintf("kas.%s", setup.Workspace.Subdomain), "is_demo": setup.IsDemo, } diff --git a/pkg/test/mocks/Client.go b/pkg/test/mocks/Client.go index fc910adb..e473cb3a 100644 --- a/pkg/test/mocks/Client.go +++ b/pkg/test/mocks/Client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.1. DO NOT EDIT. +// Code generated by mockery v2.36.1. DO NOT EDIT. package mocks @@ -222,6 +222,30 @@ func (_m *Client) CreateEvent(event *api.UserEventAttributes) error { return r0 } +// CreateInstallation provides a mock function with given fields: id +func (_m *Client) CreateInstallation(id string) (string, error) { + ret := _m.Called(id) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string) (string, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(id) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateKey provides a mock function with given fields: name, content func (_m *Client) CreateKey(name string, content string) error { ret := _m.Called(name, content) diff --git a/pkg/test/mocks/ConsoleClient.go b/pkg/test/mocks/ConsoleClient.go new file mode 100644 index 00000000..77ee71f1 --- /dev/null +++ b/pkg/test/mocks/ConsoleClient.go @@ -0,0 +1,497 @@ +// Code generated by mockery v2.36.1. DO NOT EDIT. + +package mocks + +import ( + gqlclient "github.com/pluralsh/console-client-go" + mock "github.com/stretchr/testify/mock" +) + +// ConsoleClient is an autogenerated mock type for the ConsoleClient type +type ConsoleClient struct { + mock.Mock +} + +// CreateCluster provides a mock function with given fields: attributes +func (_m *ConsoleClient) CreateCluster(attributes gqlclient.ClusterAttributes) (*gqlclient.CreateCluster, error) { + ret := _m.Called(attributes) + + var r0 *gqlclient.CreateCluster + var r1 error + if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) (*gqlclient.CreateCluster, error)); ok { + return rf(attributes) + } + if rf, ok := ret.Get(0).(func(gqlclient.ClusterAttributes) *gqlclient.CreateCluster); ok { + r0 = rf(attributes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.CreateCluster) + } + } + + if rf, ok := ret.Get(1).(func(gqlclient.ClusterAttributes) error); ok { + r1 = rf(attributes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateClusterService provides a mock function with given fields: clusterId, clusterName, attr +func (_m *ConsoleClient) CreateClusterService(clusterId *string, clusterName *string, attr gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error) { + ret := _m.Called(clusterId, clusterName, attr) + + var r0 *gqlclient.ServiceDeploymentFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string, *string, gqlclient.ServiceDeploymentAttributes) (*gqlclient.ServiceDeploymentFragment, error)); ok { + return rf(clusterId, clusterName, attr) + } + if rf, ok := ret.Get(0).(func(*string, *string, gqlclient.ServiceDeploymentAttributes) *gqlclient.ServiceDeploymentFragment); ok { + r0 = rf(clusterId, clusterName, attr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ServiceDeploymentFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string, *string, gqlclient.ServiceDeploymentAttributes) error); ok { + r1 = rf(clusterId, clusterName, attr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateProvider provides a mock function with given fields: attr +func (_m *ConsoleClient) CreateProvider(attr gqlclient.ClusterProviderAttributes) (*gqlclient.CreateClusterProvider, error) { + ret := _m.Called(attr) + + var r0 *gqlclient.CreateClusterProvider + var r1 error + if rf, ok := ret.Get(0).(func(gqlclient.ClusterProviderAttributes) (*gqlclient.CreateClusterProvider, error)); ok { + return rf(attr) + } + if rf, ok := ret.Get(0).(func(gqlclient.ClusterProviderAttributes) *gqlclient.CreateClusterProvider); ok { + r0 = rf(attr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.CreateClusterProvider) + } + } + + if rf, ok := ret.Get(1).(func(gqlclient.ClusterProviderAttributes) error); ok { + r1 = rf(attr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateProviderCredentials provides a mock function with given fields: name, attr +func (_m *ConsoleClient) CreateProviderCredentials(name string, attr gqlclient.ProviderCredentialAttributes) (*gqlclient.CreateProviderCredential, error) { + ret := _m.Called(name, attr) + + var r0 *gqlclient.CreateProviderCredential + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.ProviderCredentialAttributes) (*gqlclient.CreateProviderCredential, error)); ok { + return rf(name, attr) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.ProviderCredentialAttributes) *gqlclient.CreateProviderCredential); ok { + r0 = rf(name, attr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.CreateProviderCredential) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.ProviderCredentialAttributes) error); ok { + r1 = rf(name, attr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateRepository provides a mock function with given fields: url, privateKey, passphrase, username, password +func (_m *ConsoleClient) CreateRepository(url string, privateKey *string, passphrase *string, username *string, password *string) (*gqlclient.CreateGitRepository, error) { + ret := _m.Called(url, privateKey, passphrase, username, password) + + var r0 *gqlclient.CreateGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) (*gqlclient.CreateGitRepository, error)); ok { + return rf(url, privateKey, passphrase, username, password) + } + if rf, ok := ret.Get(0).(func(string, *string, *string, *string, *string) *gqlclient.CreateGitRepository); ok { + r0 = rf(url, privateKey, passphrase, username, password) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.CreateGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(string, *string, *string, *string, *string) error); ok { + r1 = rf(url, privateKey, passphrase, username, password) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteCluster provides a mock function with given fields: id +func (_m *ConsoleClient) DeleteCluster(id string) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteClusterService provides a mock function with given fields: serviceId +func (_m *ConsoleClient) DeleteClusterService(serviceId string) (*gqlclient.DeleteServiceDeployment, error) { + ret := _m.Called(serviceId) + + var r0 *gqlclient.DeleteServiceDeployment + var r1 error + if rf, ok := ret.Get(0).(func(string) (*gqlclient.DeleteServiceDeployment, error)); ok { + return rf(serviceId) + } + if rf, ok := ret.Get(0).(func(string) *gqlclient.DeleteServiceDeployment); ok { + r0 = rf(serviceId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.DeleteServiceDeployment) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(serviceId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteProviderCredentials provides a mock function with given fields: id +func (_m *ConsoleClient) DeleteProviderCredentials(id string) (*gqlclient.DeleteProviderCredential, error) { + ret := _m.Called(id) + + var r0 *gqlclient.DeleteProviderCredential + var r1 error + if rf, ok := ret.Get(0).(func(string) (*gqlclient.DeleteProviderCredential, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(string) *gqlclient.DeleteProviderCredential); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.DeleteProviderCredential) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCluster provides a mock function with given fields: clusterId, clusterName +func (_m *ConsoleClient) GetCluster(clusterId *string, clusterName *string) (*gqlclient.ClusterFragment, error) { + ret := _m.Called(clusterId, clusterName) + + var r0 *gqlclient.ClusterFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string, *string) (*gqlclient.ClusterFragment, error)); ok { + return rf(clusterId, clusterName) + } + if rf, ok := ret.Get(0).(func(*string, *string) *gqlclient.ClusterFragment); ok { + r0 = rf(clusterId, clusterName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ClusterFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string, *string) error); ok { + r1 = rf(clusterId, clusterName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetClusterService provides a mock function with given fields: serviceId, serviceName, clusterName +func (_m *ConsoleClient) GetClusterService(serviceId *string, serviceName *string, clusterName *string) (*gqlclient.ServiceDeploymentExtended, error) { + ret := _m.Called(serviceId, serviceName, clusterName) + + var r0 *gqlclient.ServiceDeploymentExtended + var r1 error + if rf, ok := ret.Get(0).(func(*string, *string, *string) (*gqlclient.ServiceDeploymentExtended, error)); ok { + return rf(serviceId, serviceName, clusterName) + } + if rf, ok := ret.Get(0).(func(*string, *string, *string) *gqlclient.ServiceDeploymentExtended); ok { + r0 = rf(serviceId, serviceName, clusterName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ServiceDeploymentExtended) + } + } + + if rf, ok := ret.Get(1).(func(*string, *string, *string) error); ok { + r1 = rf(serviceId, serviceName, clusterName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListClusterServices provides a mock function with given fields: clusterId, handle +func (_m *ConsoleClient) ListClusterServices(clusterId *string, handle *string) ([]*gqlclient.ServiceDeploymentEdgeFragment, error) { + ret := _m.Called(clusterId, handle) + + var r0 []*gqlclient.ServiceDeploymentEdgeFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string, *string) ([]*gqlclient.ServiceDeploymentEdgeFragment, error)); ok { + return rf(clusterId, handle) + } + if rf, ok := ret.Get(0).(func(*string, *string) []*gqlclient.ServiceDeploymentEdgeFragment); ok { + r0 = rf(clusterId, handle) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*gqlclient.ServiceDeploymentEdgeFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string, *string) error); ok { + r1 = rf(clusterId, handle) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListClusters provides a mock function with given fields: +func (_m *ConsoleClient) ListClusters() (*gqlclient.ListClusters, error) { + ret := _m.Called() + + var r0 *gqlclient.ListClusters + var r1 error + if rf, ok := ret.Get(0).(func() (*gqlclient.ListClusters, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *gqlclient.ListClusters); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ListClusters) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListProviders provides a mock function with given fields: +func (_m *ConsoleClient) ListProviders() (*gqlclient.ListProviders, error) { + ret := _m.Called() + + var r0 *gqlclient.ListProviders + var r1 error + if rf, ok := ret.Get(0).(func() (*gqlclient.ListProviders, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *gqlclient.ListProviders); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ListProviders) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRepositories provides a mock function with given fields: +func (_m *ConsoleClient) ListRepositories() (*gqlclient.ListGitRepositories, error) { + ret := _m.Called() + + var r0 *gqlclient.ListGitRepositories + var r1 error + if rf, ok := ret.Get(0).(func() (*gqlclient.ListGitRepositories, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *gqlclient.ListGitRepositories); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ListGitRepositories) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SavePipeline provides a mock function with given fields: name, attrs +func (_m *ConsoleClient) SavePipeline(name string, attrs gqlclient.PipelineAttributes) (*gqlclient.PipelineFragment, error) { + ret := _m.Called(name, attrs) + + var r0 *gqlclient.PipelineFragment + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.PipelineAttributes) (*gqlclient.PipelineFragment, error)); ok { + return rf(name, attrs) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.PipelineAttributes) *gqlclient.PipelineFragment); ok { + r0 = rf(name, attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.PipelineFragment) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.PipelineAttributes) error); ok { + r1 = rf(name, attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateCluster provides a mock function with given fields: id, attr +func (_m *ConsoleClient) UpdateCluster(id string, attr gqlclient.ClusterUpdateAttributes) (*gqlclient.UpdateCluster, error) { + ret := _m.Called(id, attr) + + var r0 *gqlclient.UpdateCluster + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) (*gqlclient.UpdateCluster, error)); ok { + return rf(id, attr) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.ClusterUpdateAttributes) *gqlclient.UpdateCluster); ok { + r0 = rf(id, attr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.UpdateCluster) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.ClusterUpdateAttributes) error); ok { + r1 = rf(id, attr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateClusterService provides a mock function with given fields: serviceId, serviceName, clusterName, attributes +func (_m *ConsoleClient) UpdateClusterService(serviceId *string, serviceName *string, clusterName *string, attributes gqlclient.ServiceUpdateAttributes) (*gqlclient.ServiceDeploymentFragment, error) { + ret := _m.Called(serviceId, serviceName, clusterName, attributes) + + var r0 *gqlclient.ServiceDeploymentFragment + var r1 error + if rf, ok := ret.Get(0).(func(*string, *string, *string, gqlclient.ServiceUpdateAttributes) (*gqlclient.ServiceDeploymentFragment, error)); ok { + return rf(serviceId, serviceName, clusterName, attributes) + } + if rf, ok := ret.Get(0).(func(*string, *string, *string, gqlclient.ServiceUpdateAttributes) *gqlclient.ServiceDeploymentFragment); ok { + r0 = rf(serviceId, serviceName, clusterName, attributes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.ServiceDeploymentFragment) + } + } + + if rf, ok := ret.Get(1).(func(*string, *string, *string, gqlclient.ServiceUpdateAttributes) error); ok { + r1 = rf(serviceId, serviceName, clusterName, attributes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRepository provides a mock function with given fields: id, attrs +func (_m *ConsoleClient) UpdateRepository(id string, attrs gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error) { + ret := _m.Called(id, attrs) + + var r0 *gqlclient.UpdateGitRepository + var r1 error + if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) (*gqlclient.UpdateGitRepository, error)); ok { + return rf(id, attrs) + } + if rf, ok := ret.Get(0).(func(string, gqlclient.GitAttributes) *gqlclient.UpdateGitRepository); ok { + r0 = rf(id, attrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gqlclient.UpdateGitRepository) + } + } + + if rf, ok := ret.Get(1).(func(string, gqlclient.GitAttributes) error); ok { + r1 = rf(id, attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Url provides a mock function with given fields: +func (_m *ConsoleClient) Url() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// NewConsoleClient creates a new instance of ConsoleClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewConsoleClient(t interface { + mock.TestingT + Cleanup(func()) +}) *ConsoleClient { + mock := &ConsoleClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/test/mocks/Kube.go b/pkg/test/mocks/Kube.go index cb9b229b..c10942b5 100644 --- a/pkg/test/mocks/Kube.go +++ b/pkg/test/mocks/Kube.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.1. DO NOT EDIT. +// Code generated by mockery v2.36.1. DO NOT EDIT. package mocks @@ -305,6 +305,20 @@ func (_m *Kube) SecretCreate(namespace string, secret *v1.Secret) (*v1.Secret, e return r0, r1 } +// SecretDelete provides a mock function with given fields: namespace, secretName +func (_m *Kube) SecretDelete(namespace string, secretName string) error { + ret := _m.Called(namespace, secretName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(namespace, secretName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SecretDeleteCollection provides a mock function with given fields: namespace, opts, listOpts func (_m *Kube) SecretDeleteCollection(namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { ret := _m.Called(namespace, opts, listOpts) diff --git a/pkg/utils/print.go b/pkg/utils/print.go index dfa14fd2..c4f7e362 100644 --- a/pkg/utils/print.go +++ b/pkg/utils/print.go @@ -10,6 +10,7 @@ import ( "github.com/fatih/color" "github.com/olekukonko/tablewriter" "golang.org/x/term" + "sigs.k8s.io/yaml" ) func ReadLine(prompt string) (string, error) { @@ -107,3 +108,33 @@ func PrintAttributes(attrs map[string]string) { } table.Render() } + +type Printer interface { + PrettyPrint() +} + +type jsonPrinter struct { + i interface{} +} + +func (this *jsonPrinter) PrettyPrint() { + s, _ := json.MarshalIndent(this.i, "", " ") + fmt.Println(string(s)) +} + +type yamlPrinter struct { + i interface{} +} + +func (this *yamlPrinter) PrettyPrint() { + s, _ := yaml.Marshal(this.i) + fmt.Println(string(s)) +} + +func NewJsonPrinter(i interface{}) Printer { + return &jsonPrinter{i: i} +} + +func NewYAMLPrinter(i interface{}) Printer { + return &yamlPrinter{i: i} +}