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 all 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://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