Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement update profile to patch a profile's HelmRelease #1445

Merged
merged 8 commits into from
Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
Skarlso marked this conversation as resolved.
Show resolved Hide resolved

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://[email protected]/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://[email protected]/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://[email protected]/owner/config-repo.git",
"--cluster", "prod",
"--version", "&%*/v",
})

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

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"))
})
})
})
73 changes: 73 additions & 0 deletions pkg/helm/releases.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
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"
apimachinery "k8s.io/apimachinery/pkg/util/yaml"
kyaml "sigs.k8s.io/yaml"
)

// MakeHelmRelease returns a HelmRelease object given a name, version, cluster, namespace, and HelmRepository's name and namespace.
Expand Down Expand Up @@ -37,3 +43,70 @@ func MakeHelmRelease(name, version, cluster, namespace string, helmRepository ty
},
}
}

// FindReleaseInNamespace iterates through a slice of HelmReleases to find one with a given name in a given namespace, and returns it with its index.
func FindReleaseInNamespace(existingReleases []helmv2beta1.HelmRelease, name, ns string) (*helmv2beta1.HelmRelease, int, error) {
aclevername marked this conversation as resolved.
Show resolved Hide resolved
for i, r := range existingReleases {
if r.Name == name && r.Namespace == ns {
return &r, i, nil
}
}

return nil, -1, nil
}

// AppendHelmReleaseToString appends a HelmRelease to a string.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:D Can you be a tiny bit more descriptive please? :) Why? How? What's the purpose of the append?

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 := apimachinery.NewYAMLOrJSONDecoder(bytes.NewReader(resources), 100000000)
aclevername marked this conversation as resolved.
Show resolved Hide resolved

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
}

func PatchHelmRelease(existingReleases []helmv2beta1.HelmRelease, patchedHelmRelease helmv2beta1.HelmRelease, index int) (string, error) {
existingReleases[index] = patchedHelmRelease
aclevername marked this conversation as resolved.
Show resolved Hide resolved

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