diff --git a/.gitignore b/.gitignore index aa286f4f7d..8b181822ba 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ ginkgo.report .deps test/library/wego-library-test tilt_modules +.envrc diff --git a/cmd/gitops/add/cmd.go b/cmd/gitops/add/cmd.go index 9720e3c07b..302d44a7b8 100644 --- a/cmd/gitops/add/cmd.go +++ b/cmd/gitops/add/cmd.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" "github.com/weaveworks/weave-gitops/cmd/gitops/add/app" "github.com/weaveworks/weave-gitops/cmd/gitops/add/clusters" + "github.com/weaveworks/weave-gitops/cmd/gitops/add/profiles" ) func GetCommand(endpoint *string, client *resty.Client) *cobra.Command { @@ -21,6 +22,7 @@ gitops add cluster`, cmd.AddCommand(clusters.ClusterCommand(endpoint, client)) cmd.AddCommand(app.Cmd) + cmd.AddCommand(profiles.AddCommand()) return cmd } diff --git a/cmd/gitops/add/profiles/cmd.go b/cmd/gitops/add/profiles/cmd.go new file mode 100644 index 0000000000..88721fe13d --- /dev/null +++ b/cmd/gitops/add/profiles/cmd.go @@ -0,0 +1,122 @@ +package profiles + +import ( + "context" + "fmt" + "math/rand" + "os" + "path/filepath" + "time" + + "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/models" + "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.AddOptions + +// AddCommand provides support for adding a profile to a cluster. +func AddCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "profile", + Short: "Add a profile to a cluster", + SilenceUsage: true, + SilenceErrors: true, + Example: ` + # Add a profile to a cluster + gitops add profile --name=podinfo --cluster=prod --version=1.0.0 --config-repo=ssh://git@github.com/owner/config-repo.git + `, + RunE: addProfileCmdRunE(), + } + + 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.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().StringVar(&opts.Kubeconfig, "kubeconfig", filepath.Join(homedir.HomeDir(), ".kube", "config"), "Absolute path to the kubeconfig file") + + requiredFlags := []string{"name", "config-repo", "cluster"} + for _, f := range requiredFlags { + if err := cobra.MarkFlagRequired(cmd.Flags(), f); err != nil { + panic(fmt.Errorf("unexpected error: %w", err)) + } + } + + return cmd +} + +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) + providerClient := internal.NewGitProviderClient(os.Stdout, os.LookupEnv, auth.NewAuthCLIHandler, log) + + if err := validateAddOptions(opts); err != nil { + return 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).Add(context.Background(), gitProvider, opts) + } +} + +func validateAddOptions(opts profiles.AddOptions) error { + if models.ApplicationNameTooLong(opts.Name) { + return fmt.Errorf("--name value is too long: %s; must be <= %d characters", + opts.Name, models.MaxKubernetesResourceNameLength) + } + + if opts.Version != "latest" { + if _, err := semver.StrictNewVersion(opts.Version); err != nil { + return fmt.Errorf("error parsing --version=%s: %w", opts.Version, err) + } + } + + return nil +} diff --git a/cmd/gitops/add/profiles/cmd_test.go b/cmd/gitops/add/profiles/cmd_test.go new file mode 100644 index 0000000000..57755f4c4c --- /dev/null +++ b/cmd/gitops/add/profiles/cmd_test.go @@ -0,0 +1,90 @@ +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 + ) + + 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{ + "add", "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, and --config-repo are not provided", func() { + cmd.SetArgs([]string{ + "add", "profile", + }) + + err := cmd.Execute() + Expect(err).To(MatchError("required flag(s) \"cluster\", \"config-repo\", \"name\" not set")) + }) + + It("fails if --name value is <= 63 characters in length", func() { + cmd.SetArgs([]string{ + "add", "profile", + "--name", "a234567890123456789012345678901234567890123456789012345678901234", + "--cluster", "cluster", + "--config-repo", "config-repo", + }) + err := cmd.Execute() + Expect(err).To(MatchError("--name value is too long: a234567890123456789012345678901234567890123456789012345678901234; must be <= 63 characters")) + }) + + It("fails if given version is not valid semver", func() { + cmd.SetArgs([]string{ + "add", "profile", + "--name", "podinfo", + "--config-repo", "ssh://git@github.com/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{ + "add", "profile", + "--unknown", "param", + }) + + err := cmd.Execute() + Expect(err).To(MatchError("unknown flag: --unknown")) + }) + }) +}) diff --git a/cmd/gitops/add/profiles/profile_suite_test.go b/cmd/gitops/add/profiles/profile_suite_test.go new file mode 100644 index 0000000000..53c59fb3c4 --- /dev/null +++ b/cmd/gitops/add/profiles/profile_suite_test.go @@ -0,0 +1,13 @@ +package profiles_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestProfile(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Profiles Suite") +} diff --git a/cmd/gitops/get/profiles/cmd.go b/cmd/gitops/get/profiles/cmd.go index 6d99ac8769..b4239e6799 100644 --- a/cmd/gitops/get/profiles/cmd.go +++ b/cmd/gitops/get/profiles/cmd.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/spf13/cobra" + "github.com/weaveworks/weave-gitops/cmd/internal" "github.com/weaveworks/weave-gitops/pkg/server" "github.com/weaveworks/weave-gitops/pkg/services/profiles" "k8s.io/client-go/kubernetes" @@ -33,7 +34,7 @@ gitops get profiles } func init() { - Cmd.Flags().StringVar(&port, "port", server.DefaultPort, "port the profiles API is running on") + Cmd.Flags().StringVar(&port, "port", server.DefaultPort, "Port the profiles API is running on") } func runCmd(cmd *cobra.Command, args []string) error { @@ -52,9 +53,8 @@ func runCmd(cmd *cobra.Command, args []string) error { return err } - return profiles.GetProfiles(context.Background(), profiles.GetOptions{ + return profiles.NewService(clientSet, internal.NewCLILogger(os.Stdout)).Get(context.Background(), profiles.GetOptions{ Namespace: ns, - ClientSet: clientSet, Writer: os.Stdout, Port: port, }) diff --git a/go.mod b/go.mod index 138d1a5d6b..b9d3b67a76 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/benbjohnson/clock v1.3.0 github.com/coreos/go-oidc/v3 v3.1.0 github.com/deepmap/oapi-codegen v1.8.1 - github.com/fluxcd/go-git-providers v0.5.2 + github.com/fluxcd/go-git-providers v0.5.3 github.com/fluxcd/helm-controller/api v0.14.1 github.com/fluxcd/kustomize-controller/api v0.18.2 github.com/fluxcd/pkg/apis/meta v0.10.1 @@ -69,6 +69,7 @@ require ( require ( github.com/ghodss/yaml v1.0.0 + github.com/go-yaml/yaml v2.1.0+incompatible github.com/gofrs/flock v0.8.1 github.com/oauth2-proxy/mockoidc v0.0.0-20210703044157-382d3faf2671 gopkg.in/square/go-jose.v2 v2.5.1 diff --git a/go.sum b/go.sum index bba87f972d..a4a8f5b506 100644 --- a/go.sum +++ b/go.sum @@ -371,8 +371,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/go-git-providers v0.5.2 h1:TA+1q6w2KgSugaZ9VXODQcYbtZyn6JH+V7g2MZtzTPA= -github.com/fluxcd/go-git-providers v0.5.2/go.mod h1:4jTHTmSx3rFGnG78KUVgFYeG6vWFnKwUSr2mi31tvp8= +github.com/fluxcd/go-git-providers v0.5.3 h1:Sg5XH+aXb6mYwITtdE4yc4yLoyFW3aVZx33qWuQb9/Q= +github.com/fluxcd/go-git-providers v0.5.3/go.mod h1:4jTHTmSx3rFGnG78KUVgFYeG6vWFnKwUSr2mi31tvp8= github.com/fluxcd/helm-controller/api v0.14.1 h1:aAWaYZxTI68SD1R2SpNJh8+hm9oBeIOa9nW4YX5qYjM= github.com/fluxcd/helm-controller/api v0.14.1/go.mod h1:NkfZ5ugs9EUUPSGHfAnNs295mf8sVKG0842aL6cFzMM= github.com/fluxcd/kustomize-controller/api v0.18.2 h1:rGu9R6PMFw3x0S6tVj/ZS54sJWW6/cdWe0Gga09e1AY= @@ -511,6 +511,8 @@ github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gG github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/flect v0.2.2 h1:PAVD7sp0KOdfswjAw9BpLCU9hXo7wFSzgpQ+zNeks/A= diff --git a/pkg/git/git.go b/pkg/git/git.go index 96ce1d53b9..3a4d45426b 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -74,6 +74,12 @@ func GetSystemQualifiedPath(clusterName string, relativePath string) string { return filepath.Join(GetSystemPath(clusterName), relativePath) } +// GetProfilesPath returns the path of the file containing the manifests of installed Profiles +// joined with the cluster's system directory +func GetProfilesPath(clusterName, profilesManifestPath string) string { + return filepath.Join(GetSystemPath(clusterName), profilesManifestPath) +} + // Git is an interface for basic Git operations on a single branch of a // remote repository. //counterfeiter:generate . Git diff --git a/pkg/gitproviders/dryrun.go b/pkg/gitproviders/dryrun.go index b701b6185c..6d24058660 100644 --- a/pkg/gitproviders/dryrun.go +++ b/pkg/gitproviders/dryrun.go @@ -57,3 +57,11 @@ func (p *dryrunProvider) GetCommits(_ context.Context, repoUrl RepoURL, targetBr func (p *dryrunProvider) GetProviderDomain() string { return p.provider.GetProviderDomain() } + +func (p *dryrunProvider) GetRepoDirFiles(_ context.Context, repoUrl RepoURL, dirPath, targetBranch string) ([]*gitprovider.CommitFile, error) { + return nil, nil +} + +func (p *dryrunProvider) MergePullRequest(ctx context.Context, repoUrl RepoURL, pullRequestNumber int, commitMesage string) error { + return nil +} diff --git a/pkg/gitproviders/gitprovidersfakes/fake_git_provider.go b/pkg/gitproviders/gitprovidersfakes/fake_git_provider.go index ac7ab19eda..b72df4eacb 100644 --- a/pkg/gitproviders/gitprovidersfakes/fake_git_provider.go +++ b/pkg/gitproviders/gitprovidersfakes/fake_git_provider.go @@ -80,6 +80,22 @@ type FakeGitProvider struct { getProviderDomainReturnsOnCall map[int]struct { result1 string } + GetRepoDirFilesStub func(context.Context, gitproviders.RepoURL, string, string) ([]*gitprovider.CommitFile, error) + getRepoDirFilesMutex sync.RWMutex + getRepoDirFilesArgsForCall []struct { + arg1 context.Context + arg2 gitproviders.RepoURL + arg3 string + arg4 string + } + getRepoDirFilesReturns struct { + result1 []*gitprovider.CommitFile + result2 error + } + getRepoDirFilesReturnsOnCall map[int]struct { + result1 []*gitprovider.CommitFile + result2 error + } GetRepoVisibilityStub func(context.Context, gitproviders.RepoURL) (*gitprovider.RepositoryVisibility, error) getRepoVisibilityMutex sync.RWMutex getRepoVisibilityArgsForCall []struct { @@ -94,6 +110,20 @@ type FakeGitProvider struct { result1 *gitprovider.RepositoryVisibility result2 error } + MergePullRequestStub func(context.Context, gitproviders.RepoURL, int, string) error + mergePullRequestMutex sync.RWMutex + mergePullRequestArgsForCall []struct { + arg1 context.Context + arg2 gitproviders.RepoURL + arg3 int + arg4 string + } + mergePullRequestReturns struct { + result1 error + } + mergePullRequestReturnsOnCall map[int]struct { + result1 error + } RepositoryExistsStub func(context.Context, gitproviders.RepoURL) (bool, error) repositoryExistsMutex sync.RWMutex repositoryExistsArgsForCall []struct { @@ -442,6 +472,73 @@ func (fake *FakeGitProvider) GetProviderDomainReturnsOnCall(i int, result1 strin }{result1} } +func (fake *FakeGitProvider) GetRepoDirFiles(arg1 context.Context, arg2 gitproviders.RepoURL, arg3 string, arg4 string) ([]*gitprovider.CommitFile, error) { + fake.getRepoDirFilesMutex.Lock() + ret, specificReturn := fake.getRepoDirFilesReturnsOnCall[len(fake.getRepoDirFilesArgsForCall)] + fake.getRepoDirFilesArgsForCall = append(fake.getRepoDirFilesArgsForCall, struct { + arg1 context.Context + arg2 gitproviders.RepoURL + arg3 string + arg4 string + }{arg1, arg2, arg3, arg4}) + stub := fake.GetRepoDirFilesStub + fakeReturns := fake.getRepoDirFilesReturns + fake.recordInvocation("GetRepoDirFiles", []interface{}{arg1, arg2, arg3, arg4}) + fake.getRepoDirFilesMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeGitProvider) GetRepoDirFilesCallCount() int { + fake.getRepoDirFilesMutex.RLock() + defer fake.getRepoDirFilesMutex.RUnlock() + return len(fake.getRepoDirFilesArgsForCall) +} + +func (fake *FakeGitProvider) GetRepoDirFilesCalls(stub func(context.Context, gitproviders.RepoURL, string, string) ([]*gitprovider.CommitFile, error)) { + fake.getRepoDirFilesMutex.Lock() + defer fake.getRepoDirFilesMutex.Unlock() + fake.GetRepoDirFilesStub = stub +} + +func (fake *FakeGitProvider) GetRepoDirFilesArgsForCall(i int) (context.Context, gitproviders.RepoURL, string, string) { + fake.getRepoDirFilesMutex.RLock() + defer fake.getRepoDirFilesMutex.RUnlock() + argsForCall := fake.getRepoDirFilesArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeGitProvider) GetRepoDirFilesReturns(result1 []*gitprovider.CommitFile, result2 error) { + fake.getRepoDirFilesMutex.Lock() + defer fake.getRepoDirFilesMutex.Unlock() + fake.GetRepoDirFilesStub = nil + fake.getRepoDirFilesReturns = struct { + result1 []*gitprovider.CommitFile + result2 error + }{result1, result2} +} + +func (fake *FakeGitProvider) GetRepoDirFilesReturnsOnCall(i int, result1 []*gitprovider.CommitFile, result2 error) { + fake.getRepoDirFilesMutex.Lock() + defer fake.getRepoDirFilesMutex.Unlock() + fake.GetRepoDirFilesStub = nil + if fake.getRepoDirFilesReturnsOnCall == nil { + fake.getRepoDirFilesReturnsOnCall = make(map[int]struct { + result1 []*gitprovider.CommitFile + result2 error + }) + } + fake.getRepoDirFilesReturnsOnCall[i] = struct { + result1 []*gitprovider.CommitFile + result2 error + }{result1, result2} +} + func (fake *FakeGitProvider) GetRepoVisibility(arg1 context.Context, arg2 gitproviders.RepoURL) (*gitprovider.RepositoryVisibility, error) { fake.getRepoVisibilityMutex.Lock() ret, specificReturn := fake.getRepoVisibilityReturnsOnCall[len(fake.getRepoVisibilityArgsForCall)] @@ -507,6 +604,70 @@ func (fake *FakeGitProvider) GetRepoVisibilityReturnsOnCall(i int, result1 *gitp }{result1, result2} } +func (fake *FakeGitProvider) MergePullRequest(arg1 context.Context, arg2 gitproviders.RepoURL, arg3 int, arg4 string) error { + fake.mergePullRequestMutex.Lock() + ret, specificReturn := fake.mergePullRequestReturnsOnCall[len(fake.mergePullRequestArgsForCall)] + fake.mergePullRequestArgsForCall = append(fake.mergePullRequestArgsForCall, struct { + arg1 context.Context + arg2 gitproviders.RepoURL + arg3 int + arg4 string + }{arg1, arg2, arg3, arg4}) + stub := fake.MergePullRequestStub + fakeReturns := fake.mergePullRequestReturns + fake.recordInvocation("MergePullRequest", []interface{}{arg1, arg2, arg3, arg4}) + fake.mergePullRequestMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeGitProvider) MergePullRequestCallCount() int { + fake.mergePullRequestMutex.RLock() + defer fake.mergePullRequestMutex.RUnlock() + return len(fake.mergePullRequestArgsForCall) +} + +func (fake *FakeGitProvider) MergePullRequestCalls(stub func(context.Context, gitproviders.RepoURL, int, string) error) { + fake.mergePullRequestMutex.Lock() + defer fake.mergePullRequestMutex.Unlock() + fake.MergePullRequestStub = stub +} + +func (fake *FakeGitProvider) MergePullRequestArgsForCall(i int) (context.Context, gitproviders.RepoURL, int, string) { + fake.mergePullRequestMutex.RLock() + defer fake.mergePullRequestMutex.RUnlock() + argsForCall := fake.mergePullRequestArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeGitProvider) MergePullRequestReturns(result1 error) { + fake.mergePullRequestMutex.Lock() + defer fake.mergePullRequestMutex.Unlock() + fake.MergePullRequestStub = nil + fake.mergePullRequestReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeGitProvider) MergePullRequestReturnsOnCall(i int, result1 error) { + fake.mergePullRequestMutex.Lock() + defer fake.mergePullRequestMutex.Unlock() + fake.MergePullRequestStub = nil + if fake.mergePullRequestReturnsOnCall == nil { + fake.mergePullRequestReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.mergePullRequestReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeGitProvider) RepositoryExists(arg1 context.Context, arg2 gitproviders.RepoURL) (bool, error) { fake.repositoryExistsMutex.Lock() ret, specificReturn := fake.repositoryExistsReturnsOnCall[len(fake.repositoryExistsArgsForCall)] @@ -653,8 +814,12 @@ func (fake *FakeGitProvider) Invocations() map[string][][]interface{} { defer fake.getDefaultBranchMutex.RUnlock() fake.getProviderDomainMutex.RLock() defer fake.getProviderDomainMutex.RUnlock() + fake.getRepoDirFilesMutex.RLock() + defer fake.getRepoDirFilesMutex.RUnlock() fake.getRepoVisibilityMutex.RLock() defer fake.getRepoVisibilityMutex.RUnlock() + fake.mergePullRequestMutex.RLock() + defer fake.mergePullRequestMutex.RUnlock() fake.repositoryExistsMutex.RLock() defer fake.repositoryExistsMutex.RUnlock() fake.uploadDeployKeyMutex.RLock() diff --git a/pkg/gitproviders/provider.go b/pkg/gitproviders/provider.go index a7a8a33ef9..257fff0b7a 100644 --- a/pkg/gitproviders/provider.go +++ b/pkg/gitproviders/provider.go @@ -38,6 +38,8 @@ type GitProvider interface { CreatePullRequest(ctx context.Context, repoUrl RepoURL, prInfo PullRequestInfo) (gitprovider.PullRequest, error) GetCommits(ctx context.Context, repoUrl RepoURL, targetBranch string, pageSize int, pageToken int) ([]gitprovider.Commit, error) GetProviderDomain() string + GetRepoDirFiles(ctx context.Context, repoUrl RepoURL, dirPath, targetBranch string) ([]*gitprovider.CommitFile, error) + MergePullRequest(ctx context.Context, repoUrl RepoURL, pullRequestNumber int, commitMesage string) error } type PullRequestInfo struct { diff --git a/pkg/gitproviders/provider_org.go b/pkg/gitproviders/provider_org.go index 703dc5a005..59c27c95a1 100644 --- a/pkg/gitproviders/provider_org.go +++ b/pkg/gitproviders/provider_org.go @@ -125,3 +125,29 @@ func (p orgGitProvider) GetCommits(ctx context.Context, repoUrl RepoURL, targetB func (p orgGitProvider) GetProviderDomain() string { return getProviderDomain(p.provider.ProviderID()) } + +// GetRepoDirFiles returns the files found in the subdirectory of a repository. +// Note that the current implementation only gets an end subdirectory. It does not get multiple directories recursively. See https://github.com/fluxcd/go-git-providers/issues/143. +func (p orgGitProvider) GetRepoDirFiles(ctx context.Context, repoUrl RepoURL, dirPath, targetBranch string) ([]*gitprovider.CommitFile, error) { + repo, err := p.getOrgRepo(ctx, repoUrl) + if err != nil { + return nil, err + } + + files, err := repo.Files().Get(ctx, dirPath, targetBranch) + if err != nil { + return nil, err + } + + return files, nil +} + +// MergePullRequest merges a pull request given the repository's URL and the PR's number with a commit message. +func (p orgGitProvider) MergePullRequest(ctx context.Context, repoUrl RepoURL, pullRequestNumber int, commitMesage string) error { + repo, err := p.getOrgRepo(ctx, repoUrl) + if err != nil { + return err + } + + return repo.PullRequests().Merge(ctx, pullRequestNumber, gitprovider.MergeMethodMerge, commitMesage) +} diff --git a/pkg/gitproviders/provider_org_test.go b/pkg/gitproviders/provider_org_test.go index ecfb3c90c5..7ac6f94a37 100644 --- a/pkg/gitproviders/provider_org_test.go +++ b/pkg/gitproviders/provider_org_test.go @@ -3,6 +3,7 @@ package gitproviders import ( "context" "errors" + "fmt" "github.com/fluxcd/go-git-providers/gitprovider" . "github.com/onsi/ginkgo" @@ -22,6 +23,7 @@ var _ = Describe("Org Provider", func() { commitClient *fakegitprovider.CommitClient branchesClient *fakegitprovider.BranchClient pullRequestsClient *fakegitprovider.PullRequestClient + fileClient *fakegitprovider.FileClient repoUrl RepoURL ) @@ -30,11 +32,13 @@ var _ = Describe("Org Provider", func() { commitClient = &fakegitprovider.CommitClient{} branchesClient = &fakegitprovider.BranchClient{} pullRequestsClient = &fakegitprovider.PullRequestClient{} + fileClient = &fakegitprovider.FileClient{} orgRepo = &fakegitprovider.OrgRepository{} orgRepo.CommitsReturns(commitClient) orgRepo.BranchesReturns(branchesClient) orgRepo.PullRequestsReturns(pullRequestsClient) + orgRepo.FilesReturns(fileClient) orgRepoClient = &fakegitprovider.OrgRepositoriesClient{} orgRepoClient.GetReturns(orgRepo, nil) @@ -313,4 +317,50 @@ var _ = Describe("Org Provider", func() { Expect(orgProvider.GetProviderDomain()).To(Equal("github.com")) }) }) + + Describe("GetRepoDirFiles", func() { + It("returns a list of files", func() { + file := &gitprovider.CommitFile{} + fileClient.GetReturns([]*gitprovider.CommitFile{file}, nil) + c, err := orgProvider.GetRepoDirFiles(context.TODO(), repoUrl, "path", "main") + Expect(err).NotTo(HaveOccurred()) + Expect(c).To(Equal([]*gitprovider.CommitFile{file})) + Expect(fileClient.GetCallCount()).To(Equal(1)) + _, dirPath, targetBranch := fileClient.GetArgsForCall(0) + Expect(dirPath).To(Equal("path")) + Expect(targetBranch).To(Equal("main")) + }) + + When("it fails to get the requested directory from the repo", func() { + It("returns an error", func() { + file := &gitprovider.CommitFile{} + fileClient.GetReturns([]*gitprovider.CommitFile{file}, fmt.Errorf("err")) + _, err := orgProvider.GetRepoDirFiles(context.TODO(), repoUrl, "path", "main") + Expect(err).To(MatchError("err")) + Expect(fileClient.GetCallCount()).To(Equal(1)) + }) + }) + }) + + Describe("MergePullRequest", func() { + It("merges a given pull request", func() { + pullRequestsClient.MergeReturns(nil) + err := orgProvider.MergePullRequest(context.TODO(), repoUrl, 1, "message") + Expect(err).NotTo(HaveOccurred()) + Expect(pullRequestsClient.MergeCallCount()).To(Equal(1)) + _, prNumber, mergeMethod, message := pullRequestsClient.MergeArgsForCall(0) + Expect(prNumber).To(Equal(1)) + Expect(mergeMethod).To(Equal(gitprovider.MergeMethodMerge)) + Expect(message).To(Equal("message")) + }) + + When("merge the PR fails", func() { + It("returns an error", func() { + pullRequestsClient.MergeReturns(fmt.Errorf("err")) + err := orgProvider.MergePullRequest(context.TODO(), repoUrl, 1, "message") + Expect(err).To(MatchError("err")) + Expect(pullRequestsClient.MergeCallCount()).To(Equal(1)) + }) + }) + }) }) diff --git a/pkg/gitproviders/provider_user.go b/pkg/gitproviders/provider_user.go index cd1823b197..2cfaffe00f 100644 --- a/pkg/gitproviders/provider_user.go +++ b/pkg/gitproviders/provider_user.go @@ -114,3 +114,29 @@ func (p userGitProvider) GetCommits(ctx context.Context, repoUrl RepoURL, target func (p userGitProvider) GetProviderDomain() string { return getProviderDomain(p.provider.ProviderID()) } + +// GetRepoDirFiles returns the files found in a directory. The dirPath must point to a directory, not a file. +// Note that the current implementation only gets an end subdirectory. It does not get multiple directories recursively. See https://github.com/fluxcd/go-git-providers/issues/143. +func (p userGitProvider) GetRepoDirFiles(ctx context.Context, repoUrl RepoURL, dirPath, targetBranch string) ([]*gitprovider.CommitFile, error) { + repo, err := p.getUserRepo(ctx, repoUrl) + if err != nil { + return nil, err + } + + files, err := repo.Files().Get(ctx, dirPath, targetBranch) + if err != nil { + return nil, err + } + + return files, nil +} + +// MergePullRequest merges a pull request given the repository's URL and the PR's number with a commit message. +func (p userGitProvider) MergePullRequest(ctx context.Context, repoUrl RepoURL, pullRequestNumber int, commitMesage string) error { + repo, err := p.getUserRepo(ctx, repoUrl) + if err != nil { + return err + } + + return repo.PullRequests().Merge(ctx, pullRequestNumber, gitprovider.MergeMethodMerge, commitMesage) +} diff --git a/pkg/gitproviders/provider_user_test.go b/pkg/gitproviders/provider_user_test.go index 72155512ff..ffb419f143 100644 --- a/pkg/gitproviders/provider_user_test.go +++ b/pkg/gitproviders/provider_user_test.go @@ -1,7 +1,9 @@ package gitproviders import ( + "context" "errors" + "fmt" "github.com/fluxcd/go-git-providers/gitprovider" . "github.com/onsi/ginkgo" @@ -20,6 +22,7 @@ var _ = Describe("User Provider", func() { commitClient *fakegitprovider.CommitClient branchesClient *fakegitprovider.BranchClient pullRequestsClient *fakegitprovider.PullRequestClient + fileClient *fakegitprovider.FileClient repoUrl RepoURL ) @@ -28,11 +31,13 @@ var _ = Describe("User Provider", func() { commitClient = &fakegitprovider.CommitClient{} branchesClient = &fakegitprovider.BranchClient{} pullRequestsClient = &fakegitprovider.PullRequestClient{} + fileClient = &fakegitprovider.FileClient{} userRepo = &fakegitprovider.UserRepository{} userRepo.CommitsReturns(commitClient) userRepo.BranchesReturns(branchesClient) userRepo.PullRequestsReturns(pullRequestsClient) + userRepo.FilesReturns(fileClient) userRepoClient = &fakegitprovider.UserRepositoriesClient{} userRepoClient.GetReturns(userRepo, nil) @@ -307,4 +312,50 @@ var _ = Describe("User Provider", func() { Expect(userProvider.GetProviderDomain()).To(Equal("github.com")) }) }) + + Describe("GetRepoDirFiles", func() { + It("returns a list of files", func() { + file := &gitprovider.CommitFile{} + fileClient.GetReturns([]*gitprovider.CommitFile{file}, nil) + c, err := userProvider.GetRepoDirFiles(context.TODO(), repoUrl, "path", "main") + Expect(err).NotTo(HaveOccurred()) + Expect(c).To(Equal([]*gitprovider.CommitFile{file})) + Expect(fileClient.GetCallCount()).To(Equal(1)) + _, dirPath, targetBranch := fileClient.GetArgsForCall(0) + Expect(dirPath).To(Equal("path")) + Expect(targetBranch).To(Equal("main")) + }) + + When("it fails to get the requested directory from the repo", func() { + It("returns an error", func() { + file := &gitprovider.CommitFile{} + fileClient.GetReturns([]*gitprovider.CommitFile{file}, fmt.Errorf("err")) + _, err := userProvider.GetRepoDirFiles(context.TODO(), repoUrl, "path", "main") + Expect(err).To(MatchError("err")) + Expect(fileClient.GetCallCount()).To(Equal(1)) + }) + }) + }) + + Describe("MergePullRequest", func() { + It("merges a given pull request", func() { + pullRequestsClient.MergeReturns(nil) + err := userProvider.MergePullRequest(context.TODO(), repoUrl, 1, "message") + Expect(err).NotTo(HaveOccurred()) + Expect(pullRequestsClient.MergeCallCount()).To(Equal(1)) + _, prNumber, mergeMethod, message := pullRequestsClient.MergeArgsForCall(0) + Expect(prNumber).To(Equal(1)) + Expect(mergeMethod).To(Equal(gitprovider.MergeMethodMerge)) + Expect(message).To(Equal("message")) + }) + + When("merge the PR fails", func() { + It("returns an error", func() { + pullRequestsClient.MergeReturns(fmt.Errorf("err")) + err := userProvider.MergePullRequest(context.TODO(), repoUrl, 1, "message") + Expect(err).To(MatchError("err")) + Expect(pullRequestsClient.MergeCallCount()).To(Equal(1)) + }) + }) + }) }) diff --git a/pkg/helm/releases.go b/pkg/helm/releases.go new file mode 100644 index 0000000000..a70b110780 --- /dev/null +++ b/pkg/helm/releases.go @@ -0,0 +1,39 @@ +package helm + +import ( + "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" +) + +// 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{ + ObjectMeta: metav1.ObjectMeta{ + Name: cluster + "-" + name, + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: helmv2beta1.GroupVersion.Identifier(), + Kind: helmv2beta1.HelmReleaseKind, + }, + Spec: helmv2beta1.HelmReleaseSpec{ + Chart: helmv2beta1.HelmChartTemplate{ + Spec: helmv2beta1.HelmChartTemplateSpec{ + Chart: name, + Version: version, + SourceRef: helmv2beta1.CrossNamespaceObjectReference{ + APIVersion: sourcev1beta1.GroupVersion.Identifier(), + Kind: sourcev1beta1.HelmRepositoryKind, + Name: helmRepository.Name, + Namespace: helmRepository.Namespace, + }, + }, + }, + Interval: metav1.Duration{Duration: time.Minute}, + }, + } +} diff --git a/pkg/helm/releases_test.go b/pkg/helm/releases_test.go new file mode 100644 index 0000000000..85c2ba7388 --- /dev/null +++ b/pkg/helm/releases_test.go @@ -0,0 +1,62 @@ +package helm_test + +import ( + "time" + + helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + sourcev1beta1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/weaveworks/weave-gitops/pkg/helm" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("MakeHelmRelease", func() { + var ( + name string + cluster string + ns string + version string + helmRepositoryNamespacedName types.NamespacedName + ) + + BeforeEach(func() { + name = "podinfo" + cluster = "prod" + ns = "weave-system" + version = "6.0.0" + helmRepositoryNamespacedName = types.NamespacedName{Name: name, Namespace: ns} + }) + + It("creates a helm release", func() { + actualHelmRelease := helm.MakeHelmRelease(name, version, cluster, ns, helmRepositoryNamespacedName) + expectedHelmRelease := &helmv2beta1.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: cluster + "-" + name, + Namespace: ns, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: helmv2beta1.GroupVersion.Identifier(), + Kind: helmv2beta1.HelmReleaseKind, + }, + Spec: helmv2beta1.HelmReleaseSpec{ + Chart: helmv2beta1.HelmChartTemplate{ + Spec: helmv2beta1.HelmChartTemplateSpec{ + Chart: name, + Version: version, + SourceRef: helmv2beta1.CrossNamespaceObjectReference{ + APIVersion: sourcev1beta1.GroupVersion.Identifier(), + Kind: sourcev1beta1.HelmRepositoryKind, + Name: helmRepositoryNamespacedName.Name, + Namespace: helmRepositoryNamespacedName.Namespace, + }, + }, + }, + Interval: metav1.Duration{Duration: time.Minute}, + }, + } + Expect(cmp.Diff(actualHelmRelease, expectedHelmRelease)).To(BeEmpty()) + }) +}) diff --git a/pkg/helm/watcher/controller/helm_watcher.go b/pkg/helm/watcher/controller/helm_watcher.go index 3a50b0fbcb..00870e49d5 100644 --- a/pkg/helm/watcher/controller/helm_watcher.go +++ b/pkg/helm/watcher/controller/helm_watcher.go @@ -192,24 +192,18 @@ func (r *HelmWatcherReconciler) checkForNewVersion(ctx context.Context, chart *p return "", err } - newVersions, err := r.convertStringListToSemanticVersionList(chart.AvailableVersions) + newVersions, err := ConvertStringListToSemanticVersionList(chart.AvailableVersions) if err != nil { return "", err } - oldVersions, err := r.convertStringListToSemanticVersionList(versions) + oldVersions, err := ConvertStringListToSemanticVersionList(versions) if err != nil { return "", err } - sortVersions := func(versions []*semver.Version) { - sort.SliceStable(versions, func(i, j int) bool { - return versions[i].GreaterThan(versions[j]) - }) - } - - sortVersions(newVersions) - sortVersions(oldVersions) + SortVersions(newVersions) + SortVersions(oldVersions) // If there are no old versions stored, it's likely that the profile didn't exist before. So we don't notify. // Same in case there are no new versions ( which is unlikely to happen, but we ward against it nevertheless ). @@ -225,7 +219,8 @@ func (r *HelmWatcherReconciler) checkForNewVersion(ctx context.Context, chart *p return "", nil } -func (r *HelmWatcherReconciler) convertStringListToSemanticVersionList(versions []string) ([]*semver.Version, error) { +// ConvertStringListToSemanticVersionList converts a slice of strings into a slice of semantic version. +func ConvertStringListToSemanticVersionList(versions []string) ([]*semver.Version, error) { var result []*semver.Version for _, v := range versions { @@ -239,3 +234,10 @@ func (r *HelmWatcherReconciler) convertStringListToSemanticVersionList(versions return result, nil } + +// SortVersions sorts semver versions in decreasing order. +func SortVersions(versions []*semver.Version) { + sort.SliceStable(versions, func(i, j int) bool { + return versions[i].GreaterThan(versions[j]) + }) +} diff --git a/pkg/models/manifest.go b/pkg/models/manifest.go index f496b7ffac..d7263e2880 100644 --- a/pkg/models/manifest.go +++ b/pkg/models/manifest.go @@ -44,6 +44,7 @@ const ( SystemKustomizationPath = "kustomization.yaml" WegoAppPath = "wego-app.yaml" WegoConfigPath = "wego-config.yaml" + WegoProfilesPath = "profiles.yaml" WegoConfigMapName = "weave-gitops-config" ) @@ -154,7 +155,7 @@ func BootstrapManifests(ctx context.Context, fluxClient flux.Flux, gitProvider g // NoClusterApplicableManifests generates all yaml files that are going to be written in the config repo and cannot be applied to the cluster directly func NoClusterApplicableManifests(params ManifestsParams) ([]Manifest, error) { - systemKustomization := CreateKustomization(params.ClusterName, params.WegoNamespace, RuntimePath, SourcePath, SystemKustResourcePath, UserKustResourcePath, WegoAppPath) + systemKustomization := CreateKustomization(params.ClusterName, params.WegoNamespace, RuntimePath, SourcePath, SystemKustResourcePath, UserKustResourcePath, WegoAppPath, WegoProfilesPath) systemKustomizationManifest, err := yaml.Marshal(systemKustomization) if err != nil { diff --git a/pkg/models/manifest_test.go b/pkg/models/manifest_test.go index cc9ccc02ac..f8f7d10de1 100644 --- a/pkg/models/manifest_test.go +++ b/pkg/models/manifest_test.go @@ -170,7 +170,7 @@ var _ = Describe("Installer", func() { Context("success case", func() { It("should pass successfully", func() { - systemKustomization := CreateKustomization(params.ClusterName, params.WegoNamespace, RuntimePath, SourcePath, SystemKustResourcePath, UserKustResourcePath, WegoAppPath) + systemKustomization := CreateKustomization(params.ClusterName, params.WegoNamespace, RuntimePath, SourcePath, SystemKustResourcePath, UserKustResourcePath, WegoAppPath, WegoProfilesPath) systemKustomizationManifest, err := yaml.Marshal(systemKustomization) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/services/install/install_test.go b/pkg/services/install/install_test.go index 1a46da071a..6dc97f256b 100644 --- a/pkg/services/install/install_test.go +++ b/pkg/services/install/install_test.go @@ -198,7 +198,7 @@ var _ = Describe("Installer", func() { wegoConfigManifest, err = yaml.Marshal(gitopsConfigMap) Expect(err).ShouldNot(HaveOccurred()) - systemKustomization := models.CreateKustomization(clusterName, testNamespace, models.RuntimePath, models.SourcePath, models.SystemKustResourcePath, models.UserKustResourcePath, models.WegoAppPath) + systemKustomization := models.CreateKustomization(clusterName, testNamespace, models.RuntimePath, models.SourcePath, models.SystemKustResourcePath, models.UserKustResourcePath, models.WegoAppPath, models.WegoProfilesPath) systemKustomizationManifest, err = yaml.Marshal(systemKustomization) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/services/profiles/add.go b/pkg/services/profiles/add.go new file mode 100644 index 0000000000..ced1a2582f --- /dev/null +++ b/pkg/services/profiles/add.go @@ -0,0 +1,197 @@ +package profiles + +import ( + "bytes" + "context" + "fmt" + "io" + + "github.com/google/uuid" + "github.com/weaveworks/weave-gitops/pkg/git" + "github.com/weaveworks/weave-gitops/pkg/gitproviders" + "github.com/weaveworks/weave-gitops/pkg/helm" + "github.com/weaveworks/weave-gitops/pkg/models" + "k8s.io/apimachinery/pkg/types" + + "github.com/fluxcd/go-git-providers/gitprovider" + "github.com/fluxcd/helm-controller/api/v2beta1" + goyaml "github.com/go-yaml/yaml" + "sigs.k8s.io/yaml" +) + +const AddCommitMessage = "Add Profile manifests" + +type AddOptions struct { + Name string + Cluster string + ConfigRepo string + Version string + ProfilesPort string + Namespace string + Kubeconfig string + AutoMerge bool +} + +// Add installs an available profile in a cluster's namespace by appending a HelmRelease to the profile manifest in the config repo, +// provided that such a HelmRelease does not exist with the same profile name and version in the same namespace and cluster. +func (s *ProfilesSvc) Add(ctx context.Context, gitProvider gitproviders.GitProvider, opts AddOptions) error { + configRepoURL, err := gitproviders.NewRepoURL(opts.ConfigRepo) + if err != nil { + return fmt.Errorf("failed to parse url: %w", err) + } + + repoExists, err := gitProvider.RepositoryExists(ctx, configRepoURL) + if err != nil { + return fmt.Errorf("failed to check whether repository exists: %w", err) + } else if !repoExists { + return fmt.Errorf("repository %q could not be found", configRepoURL) + } + + defaultBranch, err := gitProvider.GetDefaultBranch(ctx, configRepoURL) + if err != nil { + return fmt.Errorf("failed to get default branch: %w", err) + } + + availableProfile, version, err := s.GetProfile(ctx, GetOptions{ + Name: opts.Name, + Version: opts.Version, + Cluster: opts.Cluster, + Namespace: opts.Namespace, + Port: opts.ProfilesPort, + }) + if err != nil { + return fmt.Errorf("failed to get profiles from cluster: %w", err) + } + + if availableProfile.GetHelmRepository().GetName() == "" || availableProfile.GetHelmRepository().GetNamespace() == "" { + return fmt.Errorf("failed to discover HelmRepository's name and namespace") + } + + helmRepo := types.NamespacedName{ + Name: availableProfile.HelmRepository.Name, + Namespace: availableProfile.HelmRepository.Namespace, + } + + newRelease := helm.MakeHelmRelease(opts.Name, version, opts.Cluster, opts.Namespace, helmRepo) + + files, err := gitProvider.GetRepoDirFiles(ctx, configRepoURL, git.GetSystemPath(opts.Cluster), defaultBranch) + if err != nil { + return fmt.Errorf("failed to get files in '%s' for config repository %q: %s", git.GetSystemPath(opts.Cluster), configRepoURL, err) + } + + file, err := AppendProfileToFile(files, newRelease, git.GetProfilesPath(opts.Cluster, models.WegoProfilesPath)) + if err != nil { + return fmt.Errorf("failed to append HelmRelease to profiles file: %w", err) + } + + pr, err := gitProvider.CreatePullRequest(ctx, configRepoURL, gitproviders.PullRequestInfo{ + Title: fmt.Sprintf("GitOps add %s", opts.Name), + Description: fmt.Sprintf("Add manifest for %s profile", opts.Name), + CommitMessage: AddCommitMessage, + TargetBranch: defaultBranch, + NewBranch: uuid.New().String(), + Files: []gitprovider.CommitFile{file}, + }) + if err != nil { + return fmt.Errorf("failed to create pull request: %s", err) + } + + s.Logger.Actionf("Pull Request created: %s", pr.Get().WebURL) + + if opts.AutoMerge { + s.Logger.Actionf("auto-merge=true; merging PR number %v", pr.Get().Number) + + if err := gitProvider.MergePullRequest(ctx, configRepoURL, pr.Get().Number, AddCommitMessage); err != nil { + return fmt.Errorf("error auto-merging PR: %w", err) + } + } + + s.printAddSummary(opts) + + return nil +} + +func (s *ProfilesSvc) printAddSummary(opts AddOptions) { + s.Logger.Println("Adding profile:\n") + s.Logger.Println("Name: %s", opts.Name) + s.Logger.Println("Version: %s", opts.Version) + s.Logger.Println("Cluster: %s", opts.Cluster) + s.Logger.Println("Namespace: %s\n", opts.Namespace) +} + +// AppendProfileToFile appends a HelmRelease to profiles.yaml if file does not contain other HelmRelease with the same name and namespace. +func AppendProfileToFile(files []*gitprovider.CommitFile, newRelease *v2beta1.HelmRelease, path string) (gitprovider.CommitFile, error) { + var content string + + for _, f := range files { + if f.Path != nil && *f.Path == path { + if f.Content == nil || *f.Content == "" { + break + } + + manifestByteSlice, err := splitYAML([]byte(*f.Content)) + if err != nil { + return gitprovider.CommitFile{}, fmt.Errorf("error splitting %s: %w", models.WegoProfilesPath, err) + } + + for _, manifestBytes := range manifestByteSlice { + var r v2beta1.HelmRelease + if err := yaml.Unmarshal(manifestBytes, &r); err != nil { + return gitprovider.CommitFile{}, fmt.Errorf("error unmarshaling %s: %w", models.WegoProfilesPath, err) + } + + if profileIsInstalled(r, *newRelease) { + return gitprovider.CommitFile{}, fmt.Errorf("version %s of profile '%s' already exists in namespace %s", r.Spec.Chart.Spec.Version, r.Name, r.Namespace) + } + } + + content = *f.Content + + break + } + } + + helmReleaseManifest, err := yaml.Marshal(newRelease) + if err != nil { + return gitprovider.CommitFile{}, fmt.Errorf("failed to marshal new HelmRelease: %w", err) + } + + content += "\n---\n" + string(helmReleaseManifest) + + return gitprovider.CommitFile{ + Path: &path, + Content: &content, + }, nil +} + +// splitYAML splits a manifest file that may contain multiple YAML resources separated by '---' +// and validates that each element is YAML. +func splitYAML(resources []byte) ([][]byte, error) { + var splitResources [][]byte + + decoder := goyaml.NewDecoder(bytes.NewReader(resources)) + + for { + var value interface{} + if err := decoder.Decode(&value); err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + valueBytes, err := goyaml.Marshal(value) + if err != nil { + return nil, err + } + + splitResources = append(splitResources, valueBytes) + } + + return splitResources, nil +} + +func profileIsInstalled(r, newRelease v2beta1.HelmRelease) bool { + return r.Name == newRelease.Name && r.Namespace == newRelease.Namespace && r.Spec.Chart.Spec.Version == newRelease.Spec.Chart.Spec.Version +} diff --git a/pkg/services/profiles/add_test.go b/pkg/services/profiles/add_test.go new file mode 100644 index 0000000000..318ea5b836 --- /dev/null +++ b/pkg/services/profiles/add_test.go @@ -0,0 +1,314 @@ +package profiles_test + +import ( + "context" + "fmt" + + "github.com/weaveworks/weave-gitops/pkg/git" + "github.com/weaveworks/weave-gitops/pkg/gitproviders/gitprovidersfakes" + "github.com/weaveworks/weave-gitops/pkg/helm" + "github.com/weaveworks/weave-gitops/pkg/logger/loggerfakes" + "github.com/weaveworks/weave-gitops/pkg/models" + "github.com/weaveworks/weave-gitops/pkg/services/profiles" + "github.com/weaveworks/weave-gitops/pkg/vendorfakes/fakegitprovider" + + "github.com/fluxcd/go-git-providers/gitprovider" + "github.com/fluxcd/helm-controller/api/v2beta1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/testing" + "sigs.k8s.io/yaml" +) + +var addOptions profiles.AddOptions + +var _ = Describe("Add", func() { + var ( + gitProviders *gitprovidersfakes.FakeGitProvider + profilesSvc *profiles.ProfilesSvc + clientSet *fake.Clientset + fakeLogger *loggerfakes.FakeLogger + fakePR *fakegitprovider.PullRequest + ) + + BeforeEach(func() { + gitProviders = &gitprovidersfakes.FakeGitProvider{} + clientSet = fake.NewSimpleClientset() + fakeLogger = &loggerfakes.FakeLogger{} + fakePR = &fakegitprovider.PullRequest{} + profilesSvc = profiles.NewService(clientSet, fakeLogger) + + addOptions = profiles.AddOptions{ + ConfigRepo: "ssh://git@github.com/owner/config-repo.git", + Name: "podinfo", + Cluster: "prod", + Namespace: "weave-system", + Version: "latest", + } + }) + + When("the config repository exists", func() { + When("the version and HelmRepository name and namespace were discovered", func() { + JustBeforeEach(func() { + gitProviders.RepositoryExistsReturns(true, nil) + gitProviders.GetDefaultBranchReturns("main", nil) + gitProviders.GetRepoDirFilesReturns(makeTestFiles(), nil) + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapper(getProfilesResp), nil + }) + }) + + JustAfterEach(func() { + Expect(gitProviders.RepositoryExistsCallCount()).To(Equal(1)) + Expect(gitProviders.GetRepoDirFilesCallCount()).To(Equal(1)) + Expect(gitProviders.CreatePullRequestCallCount()).To(Equal(1)) + }) + + It("creates a helm release with the latest available version of the profile", func() { + fakePR.GetReturns(gitprovider.PullRequestInfo{ + WebURL: "url", + }) + gitProviders.CreatePullRequestReturns(fakePR, nil) + Expect(profilesSvc.Add(context.TODO(), gitProviders, addOptions)).Should(Succeed()) + }) + + When("auto-merge is enabled", func() { + It("merges the PR that was created", func() { + fakePR.GetReturns(gitprovider.PullRequestInfo{ + WebURL: "url", + Number: 42, + }) + gitProviders.CreatePullRequestReturns(fakePR, nil) + addOptions.AutoMerge = true + Expect(profilesSvc.Add(context.TODO(), gitProviders, addOptions)).Should(Succeed()) + }) + + When("the PR fails to be merged", func() { + It("returns an error", func() { + fakePR.GetReturns(gitprovider.PullRequestInfo{ + WebURL: "url", + }) + gitProviders.CreatePullRequestReturns(fakePR, nil) + gitProviders.MergePullRequestReturns(fmt.Errorf("err")) + addOptions.AutoMerge = true + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(MatchError("error auto-merging PR: err")) + }) + }) + }) + + When("an existing version other than 'latest' is specified", func() { + It("creates a helm release with that version", func() { + addOptions.Version = "6.0.0" + fakePR.GetReturns(gitprovider.PullRequestInfo{ + WebURL: "url", + }) + fakePR.GetReturns(gitprovider.PullRequestInfo{ + WebURL: "url", + }) + gitProviders.CreatePullRequestReturns(fakePR, nil) + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(BeNil()) + }) + }) + + When("it fails to create a pull request to write the helm release to the config repo", func() { + It("returns an error when ", func() { + gitProviders.CreatePullRequestReturns(nil, fmt.Errorf("err")) + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(MatchError("failed to create pull request: err")) + }) + }) + }) + + When("it fails to get a list of available profiles from the cluster", func() { + JustBeforeEach(func() { + gitProviders.RepositoryExistsReturns(true, nil) + gitProviders.GetRepoDirFilesReturns(makeTestFiles(), nil) + }) + + It("fails if it's unable to get a matching available profile from the cluster", func() { + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapperWithErr("nope"), nil + }) + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(MatchError("failed to get profiles from cluster: failed to make GET request to service weave-system/wego-app path \"/v1/profiles\": nope")) + Expect(gitProviders.RepositoryExistsCallCount()).To(Equal(1)) + }) + + It("fails if it's unable to discover the HelmRepository's name and namespace values", func() { + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapper(getRespWithoutHelmRepo()), nil + }) + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(MatchError("failed to discover HelmRepository's name and namespace")) + Expect(gitProviders.RepositoryExistsCallCount()).To(Equal(1)) + }) + }) + + When("it fails to find a matching version", func() { + It("returns an error", func() { + gitProviders.RepositoryExistsReturns(true, nil) + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapper(getProfilesResp), nil + }) + addOptions.Version = "7.0.0" + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(MatchError("failed to get profiles from cluster: version '7.0.0' not found for profile 'podinfo' in prod/weave-system")) + Expect(gitProviders.RepositoryExistsCallCount()).To(Equal(1)) + }) + }) + }) + + When("the config repository exists", func() { + It("fails if the --config-repo url format is wrong", func() { + addOptions = profiles.AddOptions{ + Name: "foo", + ConfigRepo: "{http:/-*wrong-url-827", + Cluster: "prod", + } + + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(MatchError("failed to parse url: could not get provider name from URL {http:/-*wrong-url-827: could not parse git repo url \"{http:/-*wrong-url-827\": parse \"{http:/-*wrong-url-827\": first path segment in URL cannot contain colon")) + }) + + It("fails if the config repo does not exist", func() { + gitProviders.RepositoryExistsReturns(false, nil) + err := profilesSvc.Add(context.TODO(), gitProviders, addOptions) + Expect(err).To(MatchError("repository \"ssh://git@github.com/owner/config-repo.git\" could not be found")) + Expect(gitProviders.RepositoryExistsCallCount()).To(Equal(1)) + }) + }) +}) + +var _ = Describe("AppendProfileToFile", func() { + var ( + newRelease *v2beta1.HelmRelease + existingFile *gitprovider.CommitFile + path string + content string + ) + + BeforeEach(func() { + newRelease = helm.MakeHelmRelease( + "podinfo", "6.0.0", "prod", "weave-system", + types.NamespacedName{Name: "helm-repo-name", Namespace: "helm-repo-namespace"}, + ) + path = git.GetProfilesPath("prod", models.WegoProfilesPath) + }) + + When("profiles.yaml does not exist", func() { + It("creates one with the new helm release", func() { + file, err := profiles.AppendProfileToFile(makeTestFiles(), newRelease, path) + Expect(err).NotTo(HaveOccurred()) + r, err := yaml.Marshal(newRelease) + Expect(err).NotTo(HaveOccurred()) + Expect(*file.Content).To(ContainSubstring(string(r))) + }) + }) + + When("profiles.yaml exists", func() { + When("the manifest contain a release with the same name in that namespace", func() { + When("the version is different", func() { + It("appends the release to the manifest", func() { + existingRelease := helm.MakeHelmRelease( + "podinfo", "6.0.1", "prod", "weave-system", + types.NamespacedName{Name: "helm-repo-name", Namespace: "helm-repo-namespace"}, + ) + r, _ := yaml.Marshal(existingRelease) + content = string(r) + file, err := profiles.AppendProfileToFile([]*gitprovider.CommitFile{{ + Path: &path, + Content: &content, + }}, newRelease, path) + Expect(err).NotTo(HaveOccurred()) + Expect(*file.Content).To(ContainSubstring(string(r))) + }) + }) + + When("the version is the same", func() { + It("fails to add the profile", func() { + existingRelease, _ := yaml.Marshal(newRelease) + content = string(existingRelease) + existingFile = &gitprovider.CommitFile{ + Path: &path, + Content: &content, + } + _, err := profiles.AppendProfileToFile([]*gitprovider.CommitFile{existingFile}, newRelease, path) + Expect(err).To(MatchError("version 6.0.0 of profile 'prod-podinfo' already exists in namespace weave-system")) + }) + }) + }) + + It("fails if the manifest contains a resource that is not a HelmRelease", func() { + content = "content" + _, err := profiles.AppendProfileToFile([]*gitprovider.CommitFile{{ + Path: &path, + Content: &content, + }}, newRelease, path) + Expect(err).To(MatchError("error unmarshaling profiles.yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type v2beta1.HelmRelease")) + }) + }) +}) + +func makeTestFiles() []*gitprovider.CommitFile { + path0 := ".weave-gitops/clusters/prod/system/wego-system.yaml" + content0 := "machine1 yaml content" + path1 := ".weave-gitops/clusters/prod/system/podinfo-helm-release.yaml" + content1 := "machine2 yaml content" + + files := []gitprovider.CommitFile{ + { + Path: &path0, + Content: &content0, + }, + { + Path: &path1, + Content: &content1, + }, + } + + commitFiles := make([]*gitprovider.CommitFile, 0) + for _, file := range files { + commitFiles = append(commitFiles, &gitprovider.CommitFile{ + Path: file.Path, + Content: file.Content, + }) + } + + return commitFiles +} + +func getRespWithoutHelmRepo() string { + return `{ + "profiles": [ + { + "name": "podinfo", + "home": "https://github.com/stefanprodan/podinfo", + "sources": [ + "https://github.com/stefanprodan/podinfo" + ], + "description": "Podinfo Helm chart for Kubernetes", + "keywords": [], + "maintainers": [ + { + "name": "stefanprodan", + "email": "stefanprodan@users.noreply.github.com", + "url": "" + } + ], + "icon": "", + "annotations": {}, + "kubeVersion": ">=1.19.0-0", + "availableVersions": [ + "6.0.0", + "6.0.1" + ] + } + ] + } + ` +} diff --git a/pkg/services/profiles/get.go b/pkg/services/profiles/get.go index b83128ff9d..62e8e6c12f 100644 --- a/pkg/services/profiles/get.go +++ b/pkg/services/profiles/get.go @@ -12,36 +12,95 @@ import ( "github.com/gogo/protobuf/jsonpb" pb "github.com/weaveworks/weave-gitops/pkg/api/profiles" -) - -const ( - wegoServiceName = "wego-app" - getProfilesPath = "/v1/profiles" + "github.com/weaveworks/weave-gitops/pkg/helm/watcher/controller" ) type GetOptions struct { + Name string + Version string + Cluster string Namespace string - ClientSet kubernetes.Interface Writer io.Writer Port string } -func GetProfiles(ctx context.Context, opts GetOptions) error { - resp, err := kubernetesDoRequest(ctx, opts.Namespace, wegoServiceName, opts.Port, getProfilesPath, opts.ClientSet) +// Get returns a list of available profiles. +func (s *ProfilesSvc) Get(ctx context.Context, opts GetOptions) error { + profiles, err := doKubeGetRequest(ctx, opts.Namespace, wegoServiceName, opts.Port, getProfilesPath, s.ClientSet) if err != nil { return err } + printProfiles(profiles, opts.Writer) + + return nil +} + +func doKubeGetRequest(ctx context.Context, namespace, serviceName, servicePort, path string, clientset kubernetes.Interface) (*pb.GetProfilesResponse, error) { + resp, err := kubernetesDoRequest(ctx, namespace, wegoServiceName, servicePort, getProfilesPath, clientset) + if err != nil { + return nil, err + } + profiles := &pb.GetProfilesResponse{} err = jsonpb.UnmarshalString(string(resp), profiles) if err != nil { - return fmt.Errorf("failed to unmarshal response: %w", err) + return nil, fmt.Errorf("failed to unmarshal response: %w", err) } - printProfiles(profiles, opts.Writer) + return profiles, nil +} - return nil +// GetProfile returns a single available profile. +func (s *ProfilesSvc) GetProfile(ctx context.Context, opts GetOptions) (*pb.Profile, string, error) { + s.Logger.Actionf("getting available profiles in %s/%s", opts.Cluster, opts.Namespace) + + profilesList, err := doKubeGetRequest(ctx, opts.Namespace, wegoServiceName, opts.Port, getProfilesPath, s.ClientSet) + if err != nil { + return nil, "", err + } + + var version string + + for _, p := range profilesList.Profiles { + if p.Name == opts.Name { + if len(p.AvailableVersions) == 0 { + return nil, "", fmt.Errorf("no version found for profile '%s' in %s/%s", p.Name, opts.Cluster, opts.Namespace) + } + + switch { + case opts.Version == "latest": + versions, err := controller.ConvertStringListToSemanticVersionList(p.AvailableVersions) + if err != nil { + return nil, "", err + } + + controller.SortVersions(versions) + version = versions[0].String() + default: + if !foundVersion(p.AvailableVersions, opts.Version) { + return nil, "", fmt.Errorf("version '%s' not found for profile '%s' in %s/%s", opts.Version, opts.Name, opts.Cluster, opts.Namespace) + } + + version = opts.Version + } + + return p, version, nil + } + } + + return nil, "", fmt.Errorf("no available profile '%s' found in %s/%s", opts.Name, opts.Cluster, opts.Namespace) +} + +func foundVersion(availableVersions []string, version string) bool { + for _, v := range availableVersions { + if v == version { + return true + } + } + + return false } func printProfiles(profiles *pb.GetProfilesResponse, w io.Writer) { diff --git a/pkg/services/profiles/get_test.go b/pkg/services/profiles/get_test.go index 2a5ac44e35..b494b4ff14 100644 --- a/pkg/services/profiles/get_test.go +++ b/pkg/services/profiles/get_test.go @@ -15,6 +15,7 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/client-go/testing" + "github.com/weaveworks/weave-gitops/pkg/logger/loggerfakes" "github.com/weaveworks/weave-gitops/pkg/services/profiles" ) @@ -38,7 +39,10 @@ const getProfilesResp = `{ "icon": "", "annotations": {}, "kubeVersion": ">=1.19.0-0", - "helmRepository": null, + "helmRepository": { + "name": "podinfo", + "namespace": "weave-system" + }, "availableVersions": [ "6.0.0", "6.0.1" @@ -48,79 +52,148 @@ const getProfilesResp = `{ } ` -var _ = Describe("GetProfiles", func() { +var _ = Describe("Get Profile(s)", func() { var ( - buffer *gbytes.Buffer - clientSet *fake.Clientset + buffer *gbytes.Buffer + clientSet *fake.Clientset + profilesSvc *profiles.ProfilesSvc + fakeLogger *loggerfakes.FakeLogger ) BeforeEach(func() { buffer = gbytes.NewBuffer() clientSet = fake.NewSimpleClientset() + fakeLogger = &loggerfakes.FakeLogger{} + profilesSvc = profiles.NewService(clientSet, fakeLogger) }) - It("prints the available profiles", func() { - clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { - return true, newFakeResponseWrapper(getProfilesResp), nil - }) + Context("Get", func() { + It("prints the available profiles", func() { + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapper(getProfilesResp), nil + }) - Expect(profiles.GetProfiles(context.TODO(), profiles.GetOptions{ - Namespace: "test-namespace", - ClientSet: clientSet, - Writer: buffer, - Port: "9001", - })).To(Succeed()) + Expect(profilesSvc.Get(context.TODO(), profiles.GetOptions{ + Namespace: "test-namespace", + Writer: buffer, + Port: "9001", + })).To(Succeed()) - Expect(string(buffer.Contents())).To(Equal(`NAME DESCRIPTION AVAILABLE_VERSIONS + Expect(string(buffer.Contents())).To(Equal(`NAME DESCRIPTION AVAILABLE_VERSIONS podinfo Podinfo Helm chart for Kubernetes 6.0.0,6.0.1 `)) - }) + }) - When("the response isn't valid", func() { - It("errors", func() { - clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { - return true, newFakeResponseWrapper("not=json"), nil + When("the response isn't valid", func() { + It("errors", func() { + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapper("not=json"), nil + }) + + err := profilesSvc.Get(context.TODO(), profiles.GetOptions{ + Namespace: "test-namespace", + Writer: buffer, + Port: "9001", + }) + Expect(err).To(MatchError(ContainSubstring("failed to unmarshal response"))) }) + }) - err := profiles.GetProfiles(context.TODO(), profiles.GetOptions{ - Namespace: "test-namespace", - ClientSet: clientSet, - Writer: buffer, - Port: "9001", + When("making the request fails", func() { + It("errors", func() { + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapperWithErr("nope"), nil + }) + + err := profilesSvc.Get(context.TODO(), profiles.GetOptions{ + Namespace: "test-namespace", + Writer: buffer, + Port: "9001", + }) + Expect(err).To(MatchError("failed to make GET request to service test-namespace/wego-app path \"/v1/profiles\": nope")) + }) + }) + + When("the request returns non-200", func() { + It("errors", func() { + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapperWithStatusCode(http.StatusNotFound), nil + }) + + err := profilesSvc.Get(context.TODO(), profiles.GetOptions{ + Namespace: "test-namespace", + Writer: buffer, + Port: "9001", + }) + Expect(err).To(MatchError("failed to make GET request to service test-namespace/wego-app path \"/v1/profiles\" status code: 404")) }) - Expect(err).To(MatchError(ContainSubstring("failed to unmarshal response"))) }) }) - When("making the request fails", func() { - It("errors", func() { + Context("GetProfile", func() { + var ( + opts profiles.GetOptions + ) + + BeforeEach(func() { + opts = profiles.GetOptions{ + Name: "podinfo", + Version: "latest", + Cluster: "prod", + Namespace: "test-namespace", + } + }) + + It("returns an available profile", func() { clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { - return true, newFakeResponseWrapperWithErr("nope"), nil + return true, newFakeResponseWrapper(getProfilesResp), nil }) + profile, version, err := profilesSvc.GetProfile(context.TODO(), opts) + Expect(err).NotTo(HaveOccurred()) + Expect(len(profile.AvailableVersions)).NotTo(BeZero()) + Expect(version).To(Equal("6.0.1")) + }) - err := profiles.GetProfiles(context.TODO(), profiles.GetOptions{ - Namespace: "test-namespace", - ClientSet: clientSet, - Writer: buffer, - Port: "9001", + It("it fails to return a list of available profiles from the cluster", func() { + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapperWithErr("nope"), nil }) + _, _, err := profilesSvc.GetProfile(context.TODO(), opts) Expect(err).To(MatchError("failed to make GET request to service test-namespace/wego-app path \"/v1/profiles\": nope")) }) - }) - When("the request returns non-200", func() { - It("errors", func() { + It("fails if no available profile was found that matches the name for the profile being added", func() { + badProfileResp := `{ + "profiles": [ + { + "name": "foo" + } + ] + } + ` clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { - return true, newFakeResponseWrapperWithStatusCode(http.StatusNotFound), nil + return true, newFakeResponseWrapper(badProfileResp), nil }) + _, _, err := profilesSvc.GetProfile(context.TODO(), opts) + Expect(err).To(MatchError("no available profile 'podinfo' found in prod/test-namespace")) + }) - err := profiles.GetProfiles(context.TODO(), profiles.GetOptions{ - Namespace: "test-namespace", - ClientSet: clientSet, - Writer: buffer, - Port: "9001", + It("fails if no available profile was found that matches the name for the profile being added", func() { + badProfileResp := `{ + "profiles": [ + { + "name": "podinfo", + "availableVersions": [ + ] + } + ] + } + ` + clientSet.AddProxyReactor("services", func(action testing.Action) (handled bool, ret restclient.ResponseWrapper, err error) { + return true, newFakeResponseWrapper(badProfileResp), nil }) - Expect(err).To(MatchError("failed to make GET request to service test-namespace/wego-app path \"/v1/profiles\" status code: 404")) + _, _, err := profilesSvc.GetProfile(context.TODO(), opts) + Expect(err).To(MatchError("no version found for profile 'podinfo' in prod/test-namespace")) }) }) }) diff --git a/pkg/services/profiles/profiles.go b/pkg/services/profiles/profiles.go new file mode 100644 index 0000000000..fde640411b --- /dev/null +++ b/pkg/services/profiles/profiles.go @@ -0,0 +1,36 @@ +package profiles + +import ( + "context" + + "github.com/weaveworks/weave-gitops/pkg/gitproviders" + "github.com/weaveworks/weave-gitops/pkg/logger" + "k8s.io/client-go/kubernetes" +) + +const ( + // ManifestFileName contains the manifests of all installed Profiles + ManifestFileName = "profiles.yaml" + + wegoServiceName = "wego-app" + getProfilesPath = "/v1/profiles" +) + +type ProfilesService interface { + // Add installs a profile on a cluster + Add(ctx context.Context, gitProvider gitproviders.GitProvider, opts AddOptions) error + // Get lists all the available profiles in a cluster + Get(ctx context.Context, opts GetOptions) error +} + +type ProfilesSvc struct { + ClientSet kubernetes.Interface + Logger logger.Logger +} + +func NewService(clientSet kubernetes.Interface, log logger.Logger) *ProfilesSvc { + return &ProfilesSvc{ + ClientSet: clientSet, + Logger: log, + } +} diff --git a/pkg/vendorfakes/fakegitprovider/file_client.go b/pkg/vendorfakes/fakegitprovider/file_client.go new file mode 100644 index 0000000000..6ac7878548 --- /dev/null +++ b/pkg/vendorfakes/fakegitprovider/file_client.go @@ -0,0 +1,121 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakegitprovider + +import ( + "context" + "sync" + + "github.com/fluxcd/go-git-providers/gitprovider" +) + +type FileClient struct { + GetStub func(context.Context, string, string) ([]*gitprovider.CommitFile, error) + getMutex sync.RWMutex + getArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + } + getReturns struct { + result1 []*gitprovider.CommitFile + result2 error + } + getReturnsOnCall map[int]struct { + result1 []*gitprovider.CommitFile + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FileClient) Get(arg1 context.Context, arg2 string, arg3 string) ([]*gitprovider.CommitFile, error) { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.GetStub + fakeReturns := fake.getReturns + fake.recordInvocation("Get", []interface{}{arg1, arg2, arg3}) + fake.getMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FileClient) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *FileClient) GetCalls(stub func(context.Context, string, string) ([]*gitprovider.CommitFile, error)) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = stub +} + +func (fake *FileClient) GetArgsForCall(i int) (context.Context, string, string) { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + argsForCall := fake.getArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FileClient) GetReturns(result1 []*gitprovider.CommitFile, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + fake.getReturns = struct { + result1 []*gitprovider.CommitFile + result2 error + }{result1, result2} +} + +func (fake *FileClient) GetReturnsOnCall(i int, result1 []*gitprovider.CommitFile, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 []*gitprovider.CommitFile + result2 error + }) + } + fake.getReturnsOnCall[i] = struct { + result1 []*gitprovider.CommitFile + result2 error + }{result1, result2} +} + +func (fake *FileClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FileClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ gitprovider.FileClient = new(FileClient) diff --git a/pkg/vendorfakes/fakegitprovider/org_repository.go b/pkg/vendorfakes/fakegitprovider/org_repository.go index 930071ee98..ae70e93e19 100644 --- a/pkg/vendorfakes/fakegitprovider/org_repository.go +++ b/pkg/vendorfakes/fakegitprovider/org_repository.go @@ -60,6 +60,16 @@ type OrgRepository struct { deployKeysReturnsOnCall map[int]struct { result1 gitprovider.DeployKeyClient } + FilesStub func() gitprovider.FileClient + filesMutex sync.RWMutex + filesArgsForCall []struct { + } + filesReturns struct { + result1 gitprovider.FileClient + } + filesReturnsOnCall map[int]struct { + result1 gitprovider.FileClient + } GetStub func() gitprovider.RepositoryInfo getMutex sync.RWMutex getArgsForCall []struct { @@ -412,6 +422,59 @@ func (fake *OrgRepository) DeployKeysReturnsOnCall(i int, result1 gitprovider.De }{result1} } +func (fake *OrgRepository) Files() gitprovider.FileClient { + fake.filesMutex.Lock() + ret, specificReturn := fake.filesReturnsOnCall[len(fake.filesArgsForCall)] + fake.filesArgsForCall = append(fake.filesArgsForCall, struct { + }{}) + stub := fake.FilesStub + fakeReturns := fake.filesReturns + fake.recordInvocation("Files", []interface{}{}) + fake.filesMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *OrgRepository) FilesCallCount() int { + fake.filesMutex.RLock() + defer fake.filesMutex.RUnlock() + return len(fake.filesArgsForCall) +} + +func (fake *OrgRepository) FilesCalls(stub func() gitprovider.FileClient) { + fake.filesMutex.Lock() + defer fake.filesMutex.Unlock() + fake.FilesStub = stub +} + +func (fake *OrgRepository) FilesReturns(result1 gitprovider.FileClient) { + fake.filesMutex.Lock() + defer fake.filesMutex.Unlock() + fake.FilesStub = nil + fake.filesReturns = struct { + result1 gitprovider.FileClient + }{result1} +} + +func (fake *OrgRepository) FilesReturnsOnCall(i int, result1 gitprovider.FileClient) { + fake.filesMutex.Lock() + defer fake.filesMutex.Unlock() + fake.FilesStub = nil + if fake.filesReturnsOnCall == nil { + fake.filesReturnsOnCall = make(map[int]struct { + result1 gitprovider.FileClient + }) + } + fake.filesReturnsOnCall[i] = struct { + result1 gitprovider.FileClient + }{result1} +} + func (fake *OrgRepository) Get() gitprovider.RepositoryInfo { fake.getMutex.Lock() ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] @@ -823,6 +886,8 @@ func (fake *OrgRepository) Invocations() map[string][][]interface{} { defer fake.deleteMutex.RUnlock() fake.deployKeysMutex.RLock() defer fake.deployKeysMutex.RUnlock() + fake.filesMutex.RLock() + defer fake.filesMutex.RUnlock() fake.getMutex.RLock() defer fake.getMutex.RUnlock() fake.pullRequestsMutex.RLock() diff --git a/pkg/vendorfakes/fakegitprovider/pull_request.go b/pkg/vendorfakes/fakegitprovider/pull_request.go new file mode 100644 index 0000000000..53b1c138a5 --- /dev/null +++ b/pkg/vendorfakes/fakegitprovider/pull_request.go @@ -0,0 +1,167 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakegitprovider + +import ( + "sync" + + "github.com/fluxcd/go-git-providers/gitprovider" +) + +type PullRequest struct { + APIObjectStub func() interface{} + aPIObjectMutex sync.RWMutex + aPIObjectArgsForCall []struct { + } + aPIObjectReturns struct { + result1 interface{} + } + aPIObjectReturnsOnCall map[int]struct { + result1 interface{} + } + GetStub func() gitprovider.PullRequestInfo + getMutex sync.RWMutex + getArgsForCall []struct { + } + getReturns struct { + result1 gitprovider.PullRequestInfo + } + getReturnsOnCall map[int]struct { + result1 gitprovider.PullRequestInfo + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *PullRequest) APIObject() interface{} { + fake.aPIObjectMutex.Lock() + ret, specificReturn := fake.aPIObjectReturnsOnCall[len(fake.aPIObjectArgsForCall)] + fake.aPIObjectArgsForCall = append(fake.aPIObjectArgsForCall, struct { + }{}) + stub := fake.APIObjectStub + fakeReturns := fake.aPIObjectReturns + fake.recordInvocation("APIObject", []interface{}{}) + fake.aPIObjectMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *PullRequest) APIObjectCallCount() int { + fake.aPIObjectMutex.RLock() + defer fake.aPIObjectMutex.RUnlock() + return len(fake.aPIObjectArgsForCall) +} + +func (fake *PullRequest) APIObjectCalls(stub func() interface{}) { + fake.aPIObjectMutex.Lock() + defer fake.aPIObjectMutex.Unlock() + fake.APIObjectStub = stub +} + +func (fake *PullRequest) APIObjectReturns(result1 interface{}) { + fake.aPIObjectMutex.Lock() + defer fake.aPIObjectMutex.Unlock() + fake.APIObjectStub = nil + fake.aPIObjectReturns = struct { + result1 interface{} + }{result1} +} + +func (fake *PullRequest) APIObjectReturnsOnCall(i int, result1 interface{}) { + fake.aPIObjectMutex.Lock() + defer fake.aPIObjectMutex.Unlock() + fake.APIObjectStub = nil + if fake.aPIObjectReturnsOnCall == nil { + fake.aPIObjectReturnsOnCall = make(map[int]struct { + result1 interface{} + }) + } + fake.aPIObjectReturnsOnCall[i] = struct { + result1 interface{} + }{result1} +} + +func (fake *PullRequest) Get() gitprovider.PullRequestInfo { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct { + }{}) + stub := fake.GetStub + fakeReturns := fake.getReturns + fake.recordInvocation("Get", []interface{}{}) + fake.getMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *PullRequest) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *PullRequest) GetCalls(stub func() gitprovider.PullRequestInfo) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = stub +} + +func (fake *PullRequest) GetReturns(result1 gitprovider.PullRequestInfo) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + fake.getReturns = struct { + result1 gitprovider.PullRequestInfo + }{result1} +} + +func (fake *PullRequest) GetReturnsOnCall(i int, result1 gitprovider.PullRequestInfo) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 gitprovider.PullRequestInfo + }) + } + fake.getReturnsOnCall[i] = struct { + result1 gitprovider.PullRequestInfo + }{result1} +} + +func (fake *PullRequest) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.aPIObjectMutex.RLock() + defer fake.aPIObjectMutex.RUnlock() + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *PullRequest) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ gitprovider.PullRequest = new(PullRequest) diff --git a/pkg/vendorfakes/fakegitprovider/user_repository.go b/pkg/vendorfakes/fakegitprovider/user_repository.go index a79c6312ea..e4ebb445bb 100644 --- a/pkg/vendorfakes/fakegitprovider/user_repository.go +++ b/pkg/vendorfakes/fakegitprovider/user_repository.go @@ -60,6 +60,16 @@ type UserRepository struct { deployKeysReturnsOnCall map[int]struct { result1 gitprovider.DeployKeyClient } + FilesStub func() gitprovider.FileClient + filesMutex sync.RWMutex + filesArgsForCall []struct { + } + filesReturns struct { + result1 gitprovider.FileClient + } + filesReturnsOnCall map[int]struct { + result1 gitprovider.FileClient + } GetStub func() gitprovider.RepositoryInfo getMutex sync.RWMutex getArgsForCall []struct { @@ -402,6 +412,59 @@ func (fake *UserRepository) DeployKeysReturnsOnCall(i int, result1 gitprovider.D }{result1} } +func (fake *UserRepository) Files() gitprovider.FileClient { + fake.filesMutex.Lock() + ret, specificReturn := fake.filesReturnsOnCall[len(fake.filesArgsForCall)] + fake.filesArgsForCall = append(fake.filesArgsForCall, struct { + }{}) + stub := fake.FilesStub + fakeReturns := fake.filesReturns + fake.recordInvocation("Files", []interface{}{}) + fake.filesMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *UserRepository) FilesCallCount() int { + fake.filesMutex.RLock() + defer fake.filesMutex.RUnlock() + return len(fake.filesArgsForCall) +} + +func (fake *UserRepository) FilesCalls(stub func() gitprovider.FileClient) { + fake.filesMutex.Lock() + defer fake.filesMutex.Unlock() + fake.FilesStub = stub +} + +func (fake *UserRepository) FilesReturns(result1 gitprovider.FileClient) { + fake.filesMutex.Lock() + defer fake.filesMutex.Unlock() + fake.FilesStub = nil + fake.filesReturns = struct { + result1 gitprovider.FileClient + }{result1} +} + +func (fake *UserRepository) FilesReturnsOnCall(i int, result1 gitprovider.FileClient) { + fake.filesMutex.Lock() + defer fake.filesMutex.Unlock() + fake.FilesStub = nil + if fake.filesReturnsOnCall == nil { + fake.filesReturnsOnCall = make(map[int]struct { + result1 gitprovider.FileClient + }) + } + fake.filesReturnsOnCall[i] = struct { + result1 gitprovider.FileClient + }{result1} +} + func (fake *UserRepository) Get() gitprovider.RepositoryInfo { fake.getMutex.Lock() ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] @@ -760,6 +823,8 @@ func (fake *UserRepository) Invocations() map[string][][]interface{} { defer fake.deleteMutex.RUnlock() fake.deployKeysMutex.RLock() defer fake.deployKeysMutex.RUnlock() + fake.filesMutex.RLock() + defer fake.filesMutex.RUnlock() fake.getMutex.RLock() defer fake.getMutex.RUnlock() fake.pullRequestsMutex.RLock() diff --git a/pkg/vendorfakes/generate.go b/pkg/vendorfakes/generate.go index 5eb2822351..007209d1e2 100644 --- a/pkg/vendorfakes/generate.go +++ b/pkg/vendorfakes/generate.go @@ -12,6 +12,8 @@ package vendorfakes //counterfeiter:generate -o fakegitprovider -fake-name CommitClient github.com/fluxcd/go-git-providers/gitprovider.CommitClient //counterfeiter:generate -o fakegitprovider -fake-name PullRequestClient github.com/fluxcd/go-git-providers/gitprovider.PullRequestClient //counterfeiter:generate -o fakegitprovider -fake-name Commit github.com/fluxcd/go-git-providers/gitprovider.Commit +//counterfeiter:generate -o fakegitprovider -fake-name FileClient github.com/fluxcd/go-git-providers/gitprovider.FileClient +//counterfeiter:generate -o fakegitprovider -fake-name PullRequest github.com/fluxcd/go-git-providers/gitprovider.PullRequest //counterfeiter:generate -o fakelogr -fake-name Logger github.com/go-logr/logr.Logger diff --git a/test/acceptance/test/profiles_test.go b/test/acceptance/test/profiles_test.go index 6a3da0b13e..83873a5e08 100644 --- a/test/acceptance/test/profiles_test.go +++ b/test/acceptance/test/profiles_test.go @@ -10,6 +10,10 @@ import ( "path/filepath" "time" + pb "github.com/weaveworks/weave-gitops/pkg/api/profiles" + "github.com/weaveworks/weave-gitops/pkg/gitproviders" + "github.com/weaveworks/weave-gitops/pkg/kube" + sourcev1beta1 "github.com/fluxcd/source-controller/api/v1beta1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -21,39 +25,40 @@ import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/homedir" "sigs.k8s.io/controller-runtime/pkg/client" - - pb "github.com/weaveworks/weave-gitops/pkg/api/profiles" - "github.com/weaveworks/weave-gitops/pkg/gitproviders" - "github.com/weaveworks/weave-gitops/pkg/kube" ) -var _ = PDescribe("Weave GitOps Profiles API", func() { +var _ = Describe("Weave GitOps Profiles API", func() { var ( namespace = "test-namespace" + clusterName string appRepoRemoteURL string tip TestInputs wegoService = "wego-app" wegoPort = "9001" clientSet *kubernetes.Clientset kClient client.Client + profileName = "podinfo" + resp []byte + statusCode int ) BeforeEach(func() { Expect(FileExists(gitopsBinaryPath)).To(BeTrue()) Expect(githubOrg).NotTo(BeEmpty()) - _, _, err := ResetOrCreateCluster(namespace, true) + var err error + clusterName, _, err = ResetOrCreateCluster(namespace, true) Expect(err).NotTo(HaveOccurred()) - private := true tip = generateTestInputs() - _ = initAndCreateEmptyRepo(tip.appRepoName, gitproviders.GitProviderGitHub, private, githubOrg) + _ = initAndCreateEmptyRepo(tip.appRepoName, gitproviders.GitProviderGitHub, true, githubOrg) clientSet, kClient = buildKubernetesClients() }) AfterEach(func() { deleteRepo(tip.appRepoName, gitproviders.GitProviderGitHub, githubOrg) + deleteWorkload(profileName, namespace) }) It("gets deployed and is accessible via the service", func() { @@ -61,12 +66,13 @@ var _ = PDescribe("Weave GitOps Profiles API", func() { appRepoRemoteURL = "git@github.com:" + githubOrg + "/" + tip.appRepoName + ".git" installAndVerifyWego(namespace, appRepoRemoteURL) deployProfilesHelmRepository(kClient, namespace) - time.Sleep(time.Second * 20) By("Getting a list of profiles") - resp, statusCode, err := kubernetesDoRequest(namespace, wegoService, wegoPort, "/v1/profiles", clientSet) + Eventually(func() int { + resp, statusCode, err = kubernetesDoRequest(namespace, wegoService, wegoPort, "/v1/profiles", clientSet) + return statusCode + }, "60s", "1s").Should(Equal(http.StatusOK)) Expect(err).NotTo(HaveOccurred()) - Expect(statusCode).To(Equal(http.StatusOK)) profiles := pb.GetProfilesResponse{} Expect(json.Unmarshal(resp, &profiles)).To(Succeed()) @@ -102,7 +108,25 @@ podinfo Podinfo Helm chart for Kubernetes 6.0.0,6.0.1 values, err := base64.StdEncoding.DecodeString(profileValues.Values) Expect(err).NotTo(HaveOccurred()) Expect(string(values)).To(ContainSubstring("# Default values for podinfo")) + + By("Adding a profile to a cluster") + stdOut, stdErr := runCommandAndReturnStringOutput(fmt.Sprintf("%s add profile --name %s --version 6.0.1 --namespace %s --cluster %s --config-repo %s --auto-merge", gitopsBinaryPath, profileName, namespace, clusterName, appRepoRemoteURL)) + Expect(stdErr).To(BeEmpty()) + Expect(stdOut).To(ContainSubstring( + fmt.Sprintf(`Adding profile: + +Name: podinfo +Version: 6.0.1 +Cluster: %s +Namespace: %s`, clusterName, namespace))) + + By("Verifying that the profile has been installed on the cluster") + Eventually(func() int { + resp, statusCode, err = kubernetesDoRequest(namespace, clusterName+"-"+profileName, "9898", "/healthz", clientSet) + return statusCode + }, "120s", "1s").Should(Equal(http.StatusOK)) }) + It("profiles are installed into a different namespace", func() { By("Installing the Profiles API and setting up the profile helm repository") appRepoRemoteURL = "git@github.com:" + githubOrg + "/" + tip.appRepoName + ".git" diff --git a/test/acceptance/test/utils.go b/test/acceptance/test/utils.go index 519c7449bb..5d0336203e 100644 --- a/test/acceptance/test/utils.go +++ b/test/acceptance/test/utils.go @@ -238,18 +238,20 @@ func ResetOrCreateClusterWithName(namespace string, deleteWegoRuntime bool, clus } if provider == "kind" { + var kindCluster string if clusterName == "" { - clusterName = provider + "-" + RandString(6) + kindCluster = RandString(6) } - log.Infof("Creating a kind cluster %s", clusterName) + clusterName = provider + "-" + kindCluster - var err error + log.Infof("Creating a kind cluster %s", kindCluster) + var err error if keepExistingClusters { - err = runCommandPassThrough([]string{}, "./scripts/kind-multi-cluster.sh", clusterName, "kindest/node:v"+k8sVersion) + err = runCommandPassThrough([]string{}, "./scripts/kind-multi-cluster.sh", kindCluster, "kindest/node:v"+k8sVersion) } else { - err = runCommandPassThrough([]string{}, "./scripts/kind-cluster.sh", clusterName, "kindest/node:v"+k8sVersion) + err = runCommandPassThrough([]string{}, "./scripts/kind-cluster.sh", kindCluster, "kindest/node:v"+k8sVersion) } if err != nil { diff --git a/tools/testcrds/helm.toolkit.fluxcd.io_helmreleases.yaml b/tools/testcrds/helm.toolkit.fluxcd.io_helmreleases.yaml index c25dc27504..d71b468e8e 100644 --- a/tools/testcrds/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/tools/testcrds/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -429,7 +429,7 @@ spec: minLength: 1 type: string optional: - description: Optional marks this ValuesReference as optional. When set, a not found error for the values reference is ignored, but any ValuesKey, TargetPath or transient error will still result in a reconciliation failure. + description: Optional marks this ValuesReference as optional. When set, a not found error for the values reference is ignored, but any ValuesKey, targetPath or transient error will still result in a reconciliation failure. type: boolean targetPath: description: TargetPath is the YAML dot notation path the value should be merged at. When set, the ValuesKey is expected to be a single flat value. Defaults to 'None', which results in the values getting merged at the root.