Skip to content

Commit

Permalink
Implement update profile to patch a profile's HelmRelease (#1445)
Browse files Browse the repository at this point in the history
* Add 'update profile' command
- Add cobra CLI command func and required flags
- Refactor Add Profile func to re-use some functionality for Update Profile
- Add acceptance test

* Add validation for updating profile
- Refactor profiles funcs to fail if profiles.yaml contains no HelmReleases
- Refactor func that appends HelmRelease to file so that update fails if HelmRelease does not exist

* Updating an installed profile's version patches it in a list of HelmReleases

* Update acceptance tests for updating profile

* Address 'update profile' review

* Remove sleep from profiles acceptance test

* Address review feedback

* Pend acceptance test 'SmokeTestLong - Verify that gitops can deploy an app with long name'
  • Loading branch information
Niki committed Feb 16, 2022
1 parent 14800da commit 4a48d1e
Show file tree
Hide file tree
Showing 18 changed files with 964 additions and 274 deletions.
8 changes: 2 additions & 6 deletions cmd/gitops/add/profiles/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ package profiles
import (
"context"
"fmt"
"math/rand"
"os"
"path/filepath"
"time"

"github.com/Masterminds/semver/v3"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -43,10 +41,10 @@ func AddCommand() *cobra.Command {

cmd.Flags().StringVar(&opts.Name, "name", "", "Name of the profile")
cmd.Flags().StringVar(&opts.Version, "version", "latest", "Version of the profile specified as semver (e.g.: 0.1.0) or as 'latest'")
cmd.Flags().StringVar(&opts.ConfigRepo, "config-repo", "", "URL of external repository (if any) which will hold automation manifests")
cmd.Flags().StringVar(&opts.ConfigRepo, "config-repo", "", "URL of the external repository that contains the automation manifests")
cmd.Flags().StringVar(&opts.Cluster, "cluster", "", "Name of the cluster to add the profile to")
cmd.Flags().StringVar(&opts.ProfilesPort, "profiles-port", server.DefaultPort, "Port the Profiles API is running on")
cmd.Flags().BoolVar(&opts.AutoMerge, "auto-merge", false, "If set, 'gitops add profile' will merge automatically into the repository's default branch")
cmd.Flags().BoolVar(&opts.AutoMerge, "auto-merge", false, "If set, 'gitops add profile' will merge automatically into the repository's branch")
cmd.Flags().StringVar(&opts.Kubeconfig, "kubeconfig", filepath.Join(homedir.HomeDir(), ".kube", "config"), "Absolute path to the kubeconfig file")

requiredFlags := []string{"name", "config-repo", "cluster"}
Expand All @@ -61,8 +59,6 @@ func AddCommand() *cobra.Command {

func addProfileCmdRunE() func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
rand.Seed(time.Now().UnixNano())

log := internal.NewCLILogger(os.Stdout)
fluxClient := flux.New(osys.New(), &runner.CLIRunner{})
factory := services.NewFactory(fluxClient, log)
Expand Down
14 changes: 3 additions & 11 deletions cmd/gitops/add/profiles/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,20 @@ package profiles_test

import (
"github.com/go-resty/resty/v2"
"github.com/jarcoal/httpmock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/spf13/cobra"
"github.com/weaveworks/weave-gitops/cmd/gitops/root"
)

var _ = Describe("Add Profiles", func() {
var (
cmd *cobra.Command
)
var _ = Describe("Add a Profile", func() {
var cmd *cobra.Command

BeforeEach(func() {
client := resty.New()
httpmock.ActivateNonDefault(client.GetClient())
cmd = root.RootCmd(client)
})

AfterEach(func() {
httpmock.DeactivateAndReset()
})

When("the flags are valid", func() {
It("accepts all known flags for adding a profile", func() {
cmd.SetArgs([]string{
Expand All @@ -42,7 +34,7 @@ var _ = Describe("Add Profiles", func() {
})

When("flags are not valid", func() {
It("fails if --name, --cluster, and --config-repo are not provided", func() {
It("fails if --name, --cluster, or --config-repo are not provided", func() {
cmd.SetArgs([]string{
"add", "profile",
})
Expand Down
2 changes: 2 additions & 0 deletions cmd/gitops/root/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/weaveworks/weave-gitops/cmd/gitops/check"
"github.com/weaveworks/weave-gitops/cmd/gitops/update"

"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -140,6 +141,7 @@ func RootCmd(client *resty.Client) *cobra.Command {
rootCmd.AddCommand(ui.NewCommand())
rootCmd.AddCommand(get.GetCommand(&options.endpoint, client))
rootCmd.AddCommand(add.GetCommand(&options.endpoint, client))
rootCmd.AddCommand(update.UpdateCommand(&options.endpoint, client))
rootCmd.AddCommand(delete.DeleteCommand(&options.endpoint, client))
rootCmd.AddCommand(resume.GetCommand())
rootCmd.AddCommand(suspend.GetCommand())
Expand Down
23 changes: 23 additions & 0 deletions cmd/gitops/update/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package update

import (
"github.com/weaveworks/weave-gitops/cmd/gitops/update/profiles"

"github.com/go-resty/resty/v2"
"github.com/spf13/cobra"
)

func UpdateCommand(endpoint *string, client *resty.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Update a Weave GitOps resource",
Example: `
# Update a profile that is installed on a cluster
gitops update profile --name=podinfo --cluster=prod --config-repo=ssh://git@github.com/owner/config-repo.git --version=1.0.0
`,
}

cmd.AddCommand(profiles.UpdateCommand())

return cmd
}
104 changes: 104 additions & 0 deletions cmd/gitops/update/profiles/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package profiles

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/Masterminds/semver/v3"
"github.com/spf13/cobra"
"github.com/weaveworks/weave-gitops/cmd/internal"
"github.com/weaveworks/weave-gitops/pkg/flux"
"github.com/weaveworks/weave-gitops/pkg/kube"
"github.com/weaveworks/weave-gitops/pkg/osys"
"github.com/weaveworks/weave-gitops/pkg/runner"
"github.com/weaveworks/weave-gitops/pkg/server"
"github.com/weaveworks/weave-gitops/pkg/services"
"github.com/weaveworks/weave-gitops/pkg/services/auth"
"github.com/weaveworks/weave-gitops/pkg/services/profiles"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)

var opts profiles.UpdateOptions

// UpdateCommand provides support for updating a profile that is installed on a cluster.
func UpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "profile",
Short: "Update a profile installation",
SilenceUsage: true,
SilenceErrors: true,
Example: `
# Update a profile that is installed on a cluster
gitops update profile --name=podinfo --cluster=prod --config-repo=ssh://git@github.com/owner/config-repo.git --version=1.0.0
`,
RunE: updateProfileCmdRunE(),
}

cmd.Flags().StringVar(&opts.Name, "name", "", "Name of the profile")
cmd.Flags().StringVar(&opts.Version, "version", "latest", "Version of the profile specified as semver (e.g.: 0.1.0) or as 'latest'")
cmd.Flags().StringVar(&opts.ConfigRepo, "config-repo", "", "URL of the external repository that contains the automation manifests")
cmd.Flags().StringVar(&opts.Cluster, "cluster", "", "Name of the cluster where the profile is installed")
cmd.Flags().StringVar(&opts.ProfilesPort, "profiles-port", server.DefaultPort, "Port the Profiles API is running on")
cmd.Flags().BoolVar(&opts.AutoMerge, "auto-merge", false, "If set, 'gitops update profile' will merge automatically into the repository's branch")
cmd.Flags().StringVar(&opts.Kubeconfig, "kubeconfig", filepath.Join(homedir.HomeDir(), ".kube", "config"), "Absolute path to the kubeconfig file")

requiredFlags := []string{"name", "config-repo", "cluster", "version"}
for _, f := range requiredFlags {
if err := cobra.MarkFlagRequired(cmd.Flags(), f); err != nil {
panic(fmt.Errorf("unexpected error: %w", err))
}
}

return cmd
}

func updateProfileCmdRunE() func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
log := internal.NewCLILogger(os.Stdout)
fluxClient := flux.New(osys.New(), &runner.CLIRunner{})
factory := services.NewFactory(fluxClient, log)
providerClient := internal.NewGitProviderClient(os.Stdout, os.LookupEnv, auth.NewAuthCLIHandler, log)

if opts.Version != "latest" {
if _, err := semver.StrictNewVersion(opts.Version); err != nil {
return fmt.Errorf("error parsing --version=%s: %w", opts.Version, err)
}
}

var err error
if opts.Namespace, err = cmd.Flags().GetString("namespace"); err != nil {
return err
}

config, err := clientcmd.BuildConfigFromFlags("", opts.Kubeconfig)
if err != nil {
return fmt.Errorf("error initializing kubernetes config: %w", err)
}

clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("error initializing kubernetes client: %w", err)
}

kubeClient, _, err := kube.NewKubeHTTPClient()
if err != nil {
return fmt.Errorf("failed to create kube client: %w", err)
}

_, gitProvider, err := factory.GetGitClients(context.Background(), kubeClient, providerClient, services.GitConfigParams{
ConfigRepo: opts.ConfigRepo,
Namespace: opts.Namespace,
IsHelmRepository: true,
DryRun: false,
})
if err != nil {
return fmt.Errorf("failed to get git clients: %w", err)
}

return profiles.NewService(clientSet, log).Update(context.Background(), gitProvider, opts)
}
}
13 changes: 13 additions & 0 deletions cmd/gitops/update/profiles/profiles_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package profiles_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestProfiles(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Profiles Suite")
}
71 changes: 71 additions & 0 deletions cmd/gitops/update/profiles/profiles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package profiles_test

import (
"github.com/weaveworks/weave-gitops/cmd/gitops/root"

"github.com/go-resty/resty/v2"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/spf13/cobra"
)

var _ = Describe("Update Profile(s)", func() {
var cmd *cobra.Command

BeforeEach(func() {
cmd = root.RootCmd(resty.New())
})

When("the flags are valid", func() {
It("accepts all known flags for updating a profile", func() {
cmd.SetArgs([]string{
"update", "profile",
"--name", "podinfo",
"--version", "0.0.1",
"--cluster", "prod",
"--namespace", "test-namespace",
"--config-repo", "https://ssh@github:test/test.git",
"--auto-merge", "true",
})

err := cmd.Execute()
Expect(err.Error()).NotTo(ContainSubstring("unknown flag"))
})
})

When("flags are not valid", func() {
It("fails if --name, --cluster, --version or --config-repo are not provided", func() {
cmd.SetArgs([]string{
"update", "profile",
})

err := cmd.Execute()
Expect(err).To(MatchError("required flag(s) \"cluster\", \"config-repo\", \"name\", \"version\" not set"))
})

It("fails if given version is not valid semver", func() {
cmd.SetArgs([]string{
"update", "profile",
"--name", "podinfo",
"--config-repo", "ssh://git@github.com/owner/config-repo.git",
"--cluster", "prod",
"--version", "&%*/v",
})

err := cmd.Execute()
Expect(err).To(MatchError(ContainSubstring("error parsing --version=&%*/v")))
})
})

When("a flag is unknown", func() {
It("fails", func() {
cmd.SetArgs([]string{
"update", "profile",
"--unknown", "param",
})

err := cmd.Execute()
Expect(err).To(MatchError("unknown flag: --unknown"))
})
})
})
64 changes: 64 additions & 0 deletions pkg/helm/releases.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package helm

import (
"bytes"
"fmt"
"io"
"strings"
"time"

helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1"
sourcev1beta1 "github.com/fluxcd/source-controller/api/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
kyaml "sigs.k8s.io/yaml"
)

const DefaultBufferSize = 2048

// MakeHelmRelease returns a HelmRelease object given a name, version, cluster, namespace, and HelmRepository's name and namespace.
func MakeHelmRelease(name, version, cluster, namespace string, helmRepository types.NamespacedName) *helmv2beta1.HelmRelease {
return &helmv2beta1.HelmRelease{
Expand Down Expand Up @@ -37,3 +45,59 @@ func MakeHelmRelease(name, version, cluster, namespace string, helmRepository ty
},
}
}

// AppendHelmReleaseToString appends "---" and a HelmRelease to string that may or may not be empty.
// This creates the content of a manifest that contains HelmReleases separated by "---".
func AppendHelmReleaseToString(content string, newRelease *helmv2beta1.HelmRelease) (string, error) {
var sb strings.Builder
if content != "" {
sb.WriteString(content + "\n")
}

helmReleaseManifest, err := kyaml.Marshal(newRelease)
if err != nil {
return "", fmt.Errorf("failed to marshal HelmRelease: %w", err)
}

sb.WriteString("---\n" + string(helmReleaseManifest))

return sb.String(), nil
}

// SplitHelmReleaseYAML splits a manifest file that contains one or more Helm Releases that may be separated by '---'.
func SplitHelmReleaseYAML(resources []byte) ([]*helmv2beta1.HelmRelease, error) {
var helmReleaseList []*helmv2beta1.HelmRelease

decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(resources), DefaultBufferSize)

for {
var value helmv2beta1.HelmRelease
if err := decoder.Decode(&value); err != nil {
if err == io.EOF {
break
}

return nil, err
}

helmReleaseList = append(helmReleaseList, &value)
}

return helmReleaseList, nil
}

// MarshalHelmReleases marshals a list of HelmReleases.
func MarshalHelmReleases(existingReleases []*helmv2beta1.HelmRelease) (string, error) {
var sb strings.Builder

for _, r := range existingReleases {
b, err := kyaml.Marshal(r)
if err != nil {
return "", fmt.Errorf("failed to marshal: %w", err)
}

sb.WriteString("---\n" + string(b))
}

return sb.String(), nil
}
Loading

0 comments on commit 4a48d1e

Please sign in to comment.