From fe4598066224e40debfb3e83f28d4baa746c85a4 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Wed, 9 Aug 2023 03:04:42 -0400 Subject: [PATCH] Implement install version command (#426) Adds `packages install` and `packages show terraform|helm` to support specific named version installs to make it easier to toggle versions. --- cmd/plural/packages.go | 86 +++++++++++++++++++++++++++++++++++++++- go.mod | 2 +- go.sum | 4 ++ pkg/api/charts.go | 4 +- pkg/api/client.go | 40 +++++++++++++++++++ pkg/api/models.go | 1 + pkg/api/repos.go | 12 ++++++ pkg/api/terraform.go | 14 +++++++ pkg/test/mocks/Client.go | 42 +++++++++++++++++++- pkg/test/mocks/Kube.go | 2 +- 10 files changed, 202 insertions(+), 5 deletions(-) diff --git a/cmd/plural/packages.go b/cmd/plural/packages.go index df29b2c4..3817ba22 100644 --- a/cmd/plural/packages.go +++ b/cmd/plural/packages.go @@ -13,10 +13,16 @@ import ( func (p *Plural) packagesCommands() []cli.Command { return []cli.Command{ + { + Name: "install", + Usage: "installs a package at a specific version", + ArgsUsage: "helm|terraform REPO NAME VSN", + Action: affirmed(requireArgs(p.installPackage, []string{"TYPE", "REPO", "NAME", "VERSION"}), "Are you sure you want to install this package?", "PLURAL_PACKAGES_INSTALL"), + }, { Name: "uninstall", Usage: "uninstall a helm or terraform package", - ArgsUsage: "TYPE REPO NAME", + ArgsUsage: "helm|terraform REPO NAME", Action: latestVersion(affirmed(requireArgs(rooted(p.uninstallPackage), []string{"TYPE", "REPO", "NAME"}), "Are you sure you want to uninstall this package?", "PLURAL_PACKAGES_UNINSTALL")), }, { @@ -25,7 +31,85 @@ func (p *Plural) packagesCommands() []cli.Command { ArgsUsage: "REPO", Action: latestVersion(requireArgs(rooted(p.listPackages), []string{"REPO"})), }, + { + Name: "show", + Usage: "Shows version information for packages within a plural repo", + Subcommands: p.showCommands(), + }, + } +} + +func (p *Plural) showCommands() []cli.Command { + return []cli.Command{ + { + Name: "helm", + Usage: "list versions for a helm chart", + ArgsUsage: "REPO NAME", + Action: requireArgs(p.showHelm, []string{"REPO", "NAME"}), + }, + { + Name: "terraform", + Usage: "list versions for a terraform module", + ArgsUsage: "REPO NAME", + Action: requireArgs(p.showTerraform, []string{"REPO", "NAME"}), + }, + } +} + +func (p *Plural) installPackage(c *cli.Context) error { + p.InitPluralClient() + tp, repo, name, vsn := c.Args().Get(0), c.Args().Get(1), c.Args().Get(2), c.Args().Get(3) + if err := p.Client.InstallVersion(tp, repo, name, vsn); err != nil { + return err } + + utils.Success("Successfully installed %s %s version %s in %s\n", tp, name, vsn, repo) + utils.Highlight("To apply the module in your cluster, you'll need to run `plural build --only %s && plural deploy", repo) + return nil +} + +func (p *Plural) showHelm(c *cli.Context) error { + p.InitPluralClient() + repo, name := c.Args().Get(0), c.Args().Get(1) + chart, err := api.FindChart(p.Client, repo, name) + if err != nil { + return err + } + + vsns, err := p.Client.GetVersions(chart.Id) + if err != nil { + return err + } + + header := []string{"Name", "Version", "App Version", "Created"} + return utils.PrintTable(vsns, header, func(vsn *api.Version) ([]string, error) { + appVsn := "" + if app, ok := vsn.Helm["appVersion"]; ok { + if v, ok := app.(string); ok { + appVsn = v + } + } + return []string{chart.Name, vsn.Version, appVsn, vsn.InsertedAt}, nil + }) +} + +func (p *Plural) showTerraform(c *cli.Context) error { + p.InitPluralClient() + repo, name := c.Args().Get(0), c.Args().Get(1) + chart, err := api.FindTerraform(p.Client, repo, name) + if err != nil { + return err + } + + vsns, err := p.Client.GetTerraformVersions(chart.Id) + if err != nil { + return err + } + + header := []string{"Name", "Version", "Created"} + return utils.PrintTable(vsns, header, func(vsn *api.Version) ([]string, error) { + return []string{chart.Name, vsn.Version, vsn.InsertedAt}, nil + }) } func (p *Plural) listPackages(c *cli.Context) error { diff --git a/go.mod b/go.mod index 0af4d208..97244db9 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/packethost/packngo v0.29.0 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 - github.com/pluralsh/gqlclient v1.6.0 + github.com/pluralsh/gqlclient v1.7.0 github.com/pluralsh/plural-operator v0.5.3 github.com/pluralsh/polly v0.1.1 github.com/rodaine/hclencoder v0.0.1 diff --git a/go.sum b/go.sum index 54af77e6..25edac80 100644 --- a/go.sum +++ b/go.sum @@ -924,6 +924,10 @@ github.com/pluralsh/controller-reconcile-helper v0.0.4 h1:1o+7qYSyoeqKFjx+WgQTxD github.com/pluralsh/controller-reconcile-helper v0.0.4/go.mod h1:AfY0gtteD6veBjmB6jiRx/aR4yevEf6K0M13/pGan/s= github.com/pluralsh/gqlclient v1.6.0 h1:7R0H98XrZdBdl8rQQGVGKkCY9iMStyrX+0lZ3zuArqo= github.com/pluralsh/gqlclient v1.6.0/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= +github.com/pluralsh/gqlclient v1.6.1 h1:E2VTJYwoEc6X3Nfm30lKDlPw+N+6l9NiPHJcRQqJ0Ng= +github.com/pluralsh/gqlclient v1.6.1/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= +github.com/pluralsh/gqlclient v1.7.0 h1:QUNVxAwu0CrkZAo9sYPav+nGxlCyvGypmDqxY2HBgoA= +github.com/pluralsh/gqlclient v1.7.0/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= github.com/pluralsh/oauth v0.9.2 h1:tM9hBK4tCnJUeCOgX0ctxBBCS3hiCDPoxkJLODtedmQ= github.com/pluralsh/oauth v0.9.2/go.mod h1:aTUw/75rzcsbvW+/TLvWtHVDXFIdtFrDtUncOq9vHyM= github.com/pluralsh/plural-operator v0.5.3 h1:GaPL3LgimfzKZNHt7zXzqYZpb0hgyW9noHYnkA+rqNs= diff --git a/pkg/api/charts.go b/pkg/api/charts.go index b062ec78..dece5cdd 100644 --- a/pkg/api/charts.go +++ b/pkg/api/charts.go @@ -153,13 +153,15 @@ func convertVersion(version *gqlclient.VersionFragment) *Version { if version.TemplateType != nil { v.TemplateType = *version.TemplateType } + if version.InsertedAt != nil { + v.InsertedAt = *version.InsertedAt + } v.Crds = make([]Crd, 0) for _, crd := range version.Crds { v.Crds = append(v.Crds, convertCrd(crd)) } v.Dependencies = convertDependencies(version.Dependencies) - return v } diff --git a/pkg/api/client.go b/pkg/api/client.go index 7264a58d..33bc2278 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -3,6 +3,7 @@ package api import ( "context" "encoding/json" + "fmt" "net/http" rawclient "github.com/Yamashou/gqlgenc/client" @@ -11,6 +12,7 @@ import ( "github.com/pluralsh/plural/pkg/config" "github.com/pluralsh/plural/pkg/utils" + "github.com/samber/lo" ) type authedTransport struct { @@ -51,6 +53,7 @@ type Client interface { UpdateVersion(spec *VersionSpec, tags []string) error GetCharts(repoId string) ([]*Chart, error) GetVersions(chartId string) ([]*Version, error) + GetTerraformVersions(id string) ([]*Version, error) GetChartInstallations(repoId string) ([]*ChartInstallation, error) GetPackageInstallations(repoId string) (charts []*ChartInstallation, tfs []*TerraformInstallation, err error) CreateCrd(repo string, chart string, file string) error @@ -89,6 +92,7 @@ type Client interface { TransferOwnership(name, email string) error Release(name string, tags []string) error Chat(history []*ChatMessage) (*ChatMessage, error) + InstallVersion(tp, repo, pkg, vsn string) error } type client struct { @@ -143,3 +147,39 @@ func GetErrorResponse(err error, methodName string) error { return errList } + +func FindChart(client Client, repo, name string) (*Chart, error) { + r, err := client.GetRepository(repo) + if err != nil { + return nil, err + } + + charts, err := client.GetCharts(r.Id) + if err != nil { + return nil, err + } + + chart, ok := lo.Find(charts, func(c *Chart) bool { return c.Name == name }) + if !ok { + return nil, fmt.Errorf("No chart found for repo %s and name %s", repo, name) + } + return chart, nil +} + +func FindTerraform(client Client, repo, name string) (*Terraform, error) { + r, err := client.GetRepository(repo) + if err != nil { + return nil, err + } + + tfs, err := client.GetTerraforma(r.Id) + if err != nil { + return nil, err + } + + tf, ok := lo.Find(tfs, func(c *Terraform) bool { return c.Name == name }) + if !ok { + return nil, fmt.Errorf("No terraform module found for repo %s and name %s", repo, name) + } + return tf, nil +} diff --git a/pkg/api/models.go b/pkg/api/models.go index 558e29b0..ec09885f 100644 --- a/pkg/api/models.go +++ b/pkg/api/models.go @@ -56,6 +56,7 @@ type Version struct { TemplateType gqlclient.TemplateType Crds []Crd Dependencies *Dependencies + InsertedAt string } type Terraform struct { diff --git a/pkg/api/repos.go b/pkg/api/repos.go index e0ac0bd2..d2ad378a 100644 --- a/pkg/api/repos.go +++ b/pkg/api/repos.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "os" "path/filepath" "strings" @@ -209,6 +210,17 @@ func (client *client) ListRepositories(query string) ([]*Repository, error) { return res, err } +func (client *client) InstallVersion(tp, repo, name, vsn string) error { + tp = strings.ToUpper(tp) + dt := gqlclient.DependencyType(tp) + if !dt.IsValid() { + return fmt.Errorf("invalid package type %s", tp) + } + + _, err := client.pluralClient.InstallVersion(context.Background(), dt, repo, name, vsn) + return err +} + func (client *client) Release(name string, tags []string) error { _, err := client.pluralClient.Release(context.Background(), name, tags) return err diff --git a/pkg/api/terraform.go b/pkg/api/terraform.go index 8c032b8c..5a1b27e0 100644 --- a/pkg/api/terraform.go +++ b/pkg/api/terraform.go @@ -27,6 +27,20 @@ func (client *client) GetTerraforma(repoId string) ([]*Terraform, error) { return terraform, err } +func (client *client) GetTerraformVersions(id string) ([]*Version, error) { + resp, err := client.pluralClient.GetTerraformVersions(client.ctx, id) + if err != nil { + return nil, err + } + + versions := make([]*Version, 0) + for _, version := range resp.Versions.Edges { + versions = append(versions, convertVersion(version.Node)) + } + + return versions, nil +} + func (client *client) GetTerraformInstallations(repoId string) ([]*TerraformInstallation, error) { resp, err := client.pluralClient.GetTerraformInstallations(client.ctx, repoId) if err != nil { diff --git a/pkg/test/mocks/Client.go b/pkg/test/mocks/Client.go index e30b45b0..d0cd4d11 100644 --- a/pkg/test/mocks/Client.go +++ b/pkg/test/mocks/Client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.30.16. DO NOT EDIT. +// Code generated by mockery v2.32.3. DO NOT EDIT. package mocks @@ -803,6 +803,32 @@ func (_m *Client) GetTerraformInstallations(repoId string) ([]*api.TerraformInst return r0, r1 } +// GetTerraformVersions provides a mock function with given fields: id +func (_m *Client) GetTerraformVersions(id string) ([]*api.Version, error) { + ret := _m.Called(id) + + var r0 []*api.Version + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]*api.Version, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(string) []*api.Version); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*api.Version) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetTerraforma provides a mock function with given fields: repoId func (_m *Client) GetTerraforma(repoId string) ([]*api.Terraform, error) { ret := _m.Called(repoId) @@ -974,6 +1000,20 @@ func (_m *Client) InstallRecipe(id string) error { return r0 } +// InstallVersion provides a mock function with given fields: tp, repo, pkg, vsn +func (_m *Client) InstallVersion(tp string, repo string, pkg string, vsn string) error { + ret := _m.Called(tp, repo, pkg, vsn) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok { + r0 = rf(tp, repo, pkg, vsn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ListArtifacts provides a mock function with given fields: repo func (_m *Client) ListArtifacts(repo string) ([]api.Artifact, error) { ret := _m.Called(repo) diff --git a/pkg/test/mocks/Kube.go b/pkg/test/mocks/Kube.go index 3b7ec01a..9ae81d38 100644 --- a/pkg/test/mocks/Kube.go +++ b/pkg/test/mocks/Kube.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.30.16. DO NOT EDIT. +// Code generated by mockery v2.32.3. DO NOT EDIT. package mocks