diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 9ab50cd06..7c883c548 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -15,8 +15,8 @@ limitations under the License. */ // Package v1alpha1 contains API Schema definitions for the gitops v1alpha1 API group -//+kubebuilder:object:generate=true -//+groupName=gitops.hybrid-cloud-patterns.io +// +kubebuilder:object:generate=true +// +groupName=gitops.hybrid-cloud-patterns.io package v1alpha1 import ( diff --git a/api/v1alpha1/pattern_types.go b/api/v1alpha1/pattern_types.go index 61b5c5c30..3d3a71af9 100644 --- a/api/v1alpha1/pattern_types.go +++ b/api/v1alpha1/pattern_types.go @@ -71,6 +71,9 @@ type PatternSpec struct { // Look for external changes every N minutes // ReconcileMinutes int `json:"reconcileMinutes,omitempty"` + + // Disable pre-existing subscriptions + DynamicSubscriptions bool `json:"dynamicSubscriptions"` } type GitConfig struct { @@ -78,9 +81,9 @@ type GitConfig struct { Hostname string `json:"hostname,omitempty"` //Account string `json:"account,omitempty"` - //TokenSecret string `json:"tokenSecret,omitempty"` - //TokenSecretNamespace string `json:"tokenSecretNamespace,omitempty"` - //TokenSecretKey string `json:"tokenSecretKey,omitempty"` + TokenSecret string `json:"tokenSecret,omitempty"` + TokenSecretNamespace string `json:"tokenSecretNamespace,omitempty"` + TokenSecretKey string `json:"tokenSecretKey,omitempty"` // Upstream git repo containing the pattern to deploy. Used when in-cluster fork to point to the upstream pattern repository //+operator-sdk:csv:customresourcedefinitions:type=spec @@ -149,6 +152,9 @@ type PatternStatus struct { //+operator-sdk:csv:customresourcedefinitions:type=status Version int `json:"version,omitempty"` + Path string `json:"path,omitempty"` + Revision string `json:"revision,omitempty"` + //+operator-sdk:csv:customresourcedefinitions:type=status ClusterName string `json:"clusterName,omitempty"` //+operator-sdk:csv:customresourcedefinitions:type=status diff --git a/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml b/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml index 01759ef4f..de0f3978a 100644 --- a/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml +++ b/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml @@ -49,6 +49,9 @@ spec: properties: clusterGroupName: type: string + dynamicSubscriptions: + description: Disable pre-existing subscriptions + type: boolean extraParameters: description: '.Name is dot separated per the helm --set syntax, such as: global.something.field' @@ -121,11 +124,19 @@ spec: description: 'Branch, tag, or commit to deploy. Does not support short-sha''s. Default: HEAD' type: string + tokenSecret: + description: Account string `json:"account,omitempty"` + type: string + tokenSecretKey: + type: string + tokenSecretNamespace: + type: string required: - targetRepo type: object required: - clusterGroupName + - dynamicSubscriptions - gitSpec type: object status: @@ -177,6 +188,10 @@ spec: lastStep: description: Last action related to the pattern type: string + path: + type: string + revision: + type: string version: description: Number of updates to the pattern type: integer diff --git a/controllers/argo.go b/controllers/argo.go index 9347909a5..f7a7cec7e 100644 --- a/controllers/argo.go +++ b/controllers/argo.go @@ -26,11 +26,12 @@ import ( argoapi "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" argoclient "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" + olmclient "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" api "github.com/hybrid-cloud-patterns/patterns-operator/api/v1alpha1" ) -func newApplicationParameters(p api.Pattern) []argoapi.HelmParameter { +func newApplicationParameters(p api.Pattern, valueFiles []string, client olmclient.Interface) []argoapi.HelmParameter { parameters := []argoapi.HelmParameter{ { @@ -76,6 +77,17 @@ func newApplicationParameters(p api.Pattern) []argoapi.HelmParameter { }, } + if len(valueFiles) > 0 && client != nil && p.Spec.DynamicSubscriptions { + log.Println("Spec: ", p.Spec) + excludeList := buildLiveSubscriptionExclusions(p, valueFiles, client) + for _, excludeParam := range excludeList { + parameters = append(parameters, argoapi.HelmParameter{ + Name: excludeParam, + Value: "1", + }) + } + } + for _, extra := range p.Spec.ExtraParameters { if !updateHelmParameter(extra, parameters) { log.Printf("Parameter %q = %q added", extra.Name, extra.Value) @@ -114,12 +126,13 @@ func newApplicationValueFiles(p api.Pattern) []string { return files } -func newApplication(p api.Pattern) *argoapi.Application { +func newApplication(p api.Pattern, client olmclient.Interface) *argoapi.Application { // Argo uses... // r := regexp.MustCompile("(/|:)") // root := filepath.Join(os.TempDir(), r.ReplaceAllString(NormalizeGitURL(rawRepoURL), "_")) + valueFiles := newApplicationValueFiles(p) spec := argoapi.ApplicationSpec{ // Source is a reference to the location of the application's manifests or chart @@ -128,10 +141,10 @@ func newApplication(p api.Pattern) *argoapi.Application { Path: "common/clustergroup", TargetRevision: p.Spec.GitConfig.TargetRevision, Helm: &argoapi.ApplicationSourceHelm{ - ValueFiles: newApplicationValueFiles(p), + ValueFiles: valueFiles, // Parameters is a list of Helm parameters which are passed to the helm template command upon manifest generation - Parameters: newApplicationParameters(p), + Parameters: newApplicationParameters(p, valueFiles, client), // ReleaseName is the Helm release name to use. If omitted it will use the application name // ReleaseName string `json:"releaseName,omitempty" protobuf:"bytes,3,opt,name=releaseName"` diff --git a/controllers/checkout.go b/controllers/checkout.go new file mode 100644 index 000000000..80bb9b2c6 --- /dev/null +++ b/controllers/checkout.go @@ -0,0 +1,236 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + // "log" + "fmt" + "os" + + "path/filepath" + + "github.com/go-git/go-git/v5" + // . "github.com/go-git/go-git/v5/_examples" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" +) + +// https://github.com/go-git/go-git/blob/master/_examples/commit/main.go + +func checkout(url, directory, token, branch, commit string) error { + + if err := cloneRepo(url, directory, token); err != nil { + return err + } + + if len(commit) == 0 { + // Nothing more to do + return nil + } + + if err := checkoutRevision(directory, token, commit); err != nil { + return err + } + + return nil +} + +func getHashFromReference(repo *git.Repository, name plumbing.ReferenceName) (plumbing.Hash, error) { + b, err := repo.Reference(name, true) + if err != nil { + return plumbing.ZeroHash, err + } + + if !b.Name().IsTag() { + return b.Hash(), nil + } + + o, err := repo.Object(plumbing.AnyObject, b.Hash()) + if err != nil { + return plumbing.ZeroHash, err + } + + switch o := o.(type) { + case *object.Tag: + if o.TargetType != plumbing.CommitObject { + return plumbing.ZeroHash, fmt.Errorf("unsupported tag object target %q", o.TargetType) + } + + return o.Target, nil + case *object.Commit: + return o.Hash, nil + } + + return plumbing.ZeroHash, fmt.Errorf("unsupported tag target %q", o.Type()) +} + +func getCommitFromTarget(repo *git.Repository, name string) (plumbing.Hash, error) { + if len(name) == 0 { + return getHashFromReference(repo, plumbing.NewBranchReferenceName("main")) + } + + // Try as commit hash + h := plumbing.NewHash(name) + _, err := repo.Object(plumbing.AnyObject, h) + if err == nil { + return h, err + } + + // Try various reference types... + + if h, err := getHashFromReference(repo, plumbing.NewBranchReferenceName(name)); err == nil { + return h, err + } + + if h, err := getHashFromReference(repo, plumbing.NewTagReferenceName(name)); err == nil { + return h, err + } + + if h, err := getHashFromReference(repo, plumbing.NewRemoteHEADReferenceName(name)); err == nil { + return h, err + } + + return plumbing.ZeroHash, fmt.Errorf("unknown target %q", name) +} + +func checkoutRevision(directory, token, commit string) error { + + fmt.Printf("Accessing %s\n", directory) + repo, err := git.PlainOpen(directory) + if err != nil { + return err + } + + var foptions = &git.FetchOptions{ + Force: true, + InsecureSkipTLS: true, + Tags: git.AllTags, + } + + if len(token) > 0 { + // The intended use of a GitHub personal access token is in replace of your password + // because access tokens can easily be revoked. + // https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ + foptions.Auth = &http.BasicAuth{ + Username: "abc123", // yes, this can be anything except an empty string + Password: token, + } + } + + if err := repo.Fetch(foptions); err != nil && err != git.NoErrAlreadyUpToDate { + fmt.Println("Error fetching") + return err + } + + w, err := repo.Worktree() + if err != nil { + fmt.Println("Error obtaining worktree") + return err + } + + h, err := getCommitFromTarget(repo, commit) + coptions := git.CheckoutOptions{ + Force: true, + Hash: h, + } + + if err != nil { + return err + } + + fmt.Printf("git checkout %s (%s)\n", h, commit) + + if err := w.Checkout(&coptions); err != nil && err != git.NoErrAlreadyUpToDate { + fmt.Printf("Error during checkout") + return err + } + + // ... retrieving the commit being pointed by HEAD, it shows that the + // repository is pointing to the giving commit in detached mode + fmt.Println("git show-ref --head HEAD") + ref, err := repo.Head() + if err != nil { + fmt.Println("Error obtaining HEAD") + return err + } + + fmt.Printf("%s\n", ref.Hash()) + return err + +} + +func cloneRepo(url string, directory string, token string) error { + + gitDir := filepath.Join(directory, ".git") + if _, err := os.Stat(gitDir); err == nil { + fmt.Printf("%s already exists\n", gitDir) + return nil + } + fmt.Printf("git clone %s into %s\n", url, directory) + + // Clone the given repository to the given directory + var options = &git.CloneOptions{ + URL: url, + Progress: os.Stdout, + Depth: 0, + // ReferenceName: plumbing.ReferenceName, + } + + if len(token) > 0 { + // The intended use of a GitHub personal access token is in replace of your password + // because access tokens can easily be revoked. + // https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ + options.Auth = &http.BasicAuth{ + Username: "abc123", // yes, this can be anything except an empty string + Password: token, + } + } + + if err := os.MkdirAll(directory, os.ModePerm); err != nil { + return err + } + + repo, err := git.PlainClone(directory, false, options) + if err != nil { + return err + } + + // ... retrieving the commit being pointed by HEAD + fmt.Println("git show-ref --head HEAD") + if ref, err := repo.Head(); err != nil { + return err + } else { + fmt.Printf("%s\n", ref.Hash()) + } + return nil +} + +func repoHash(directory string) (error, string) { + repo, err := git.PlainOpen(directory) + if err != nil { + return err, "" + } + + // ... checking out to commit + ref, err := repo.Head() + if err != nil { + return err, "" + } + + return nil, ref.Hash().String() +} diff --git a/controllers/git.go b/controllers/drfit.go similarity index 100% rename from controllers/git.go rename to controllers/drfit.go diff --git a/controllers/pattern_controller.go b/controllers/pattern_controller.go index a738f4ce5..c05dd93a0 100644 --- a/controllers/pattern_controller.go +++ b/controllers/pattern_controller.go @@ -20,19 +20,24 @@ import ( "context" "fmt" "log" + "os" "strings" "time" + "path/filepath" + "github.com/Masterminds/semver/v3" "github.com/go-errors/errors" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" - "k8s.io/client-go/dynamic" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -149,6 +154,25 @@ func (r *PatternReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return r.actionPerformed(qualifiedInstance, "applying defaults", err) } + if qualifiedInstance.Spec.DynamicSubscriptions { + var token = "" + if len(qualifiedInstance.Spec.GitConfig.TokenSecret) > 0 { + if err, token = r.authTokenFromSecret(qualifiedInstance.Spec.GitConfig.TokenSecretNamespace, qualifiedInstance.Spec.GitConfig.TokenSecret, qualifiedInstance.Spec.GitConfig.TokenSecretKey); err != nil { + return r.actionPerformed(qualifiedInstance, "obtaining git auth token", err) + } + } + + gitDir := filepath.Join(qualifiedInstance.Status.Path, ".git") + if _, err := os.Stat(gitDir); os.IsNotExist(err) { + err := cloneRepo(qualifiedInstance.Spec.GitConfig.TargetRepo, qualifiedInstance.Status.Path, token) + return r.actionPerformed(qualifiedInstance, "cloning pattern repo", err) + } + + if err := checkoutRevision(qualifiedInstance.Status.Path, token, qualifiedInstance.Spec.GitConfig.TargetRevision); err != nil { + return r.actionPerformed(qualifiedInstance, "checkout target revision", err) + } + } + if err := r.preValidation(qualifiedInstance); err != nil { return r.actionPerformed(qualifiedInstance, "prerequisite validation", err) } @@ -208,7 +232,7 @@ func (r *PatternReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct logOnce("namespace found") // -- ArgoCD Application - targetApp := newApplication(*qualifiedInstance) + targetApp := newApplication(*qualifiedInstance, r.olmClient) _ = controllerutil.SetOwnerReference(qualifiedInstance, targetApp, r.Scheme) //log.Printf("Targeting: %s\n", objectYaml(targetApp)) @@ -380,9 +404,32 @@ func (r *PatternReconciler) applyDefaults(input *api.Pattern) (error, *api.Patte output.Spec.GitConfig.PollInterval = 180 } + if len(output.Status.Path) == 0 { + output.Status.Path = filepath.Join(os.TempDir(), output.Namespace, output.Name) + } + return nil, output } +func (r *PatternReconciler) authTokenFromSecret(namespace, secret, key string) (error, string) { + if len(key) == 0 { + return nil, "" + } + tokenSecret := &corev1.Secret{} + err := r.Client.Get(context.TODO(), types.NamespacedName{Name: secret, Namespace: namespace}, tokenSecret) + if err != nil { + // if tokenSecret, err = r.Client.Core().Secrets(namespace).Get(secret); err != nil { + r.logger.Error(err, fmt.Sprintf("Could not obtain secret %s/%s", secret, namespace)) + return err, "" + } + + if val, ok := tokenSecret.Data[key]; ok { + // See also https://github.com/kubernetes/client-go/issues/198 + return nil, string(val) + } + return errors.New(fmt.Errorf("No key '%s' found in %s/%s", key, secret, namespace)), "" +} + func (r *PatternReconciler) finalizeObject(instance *api.Pattern) error { // Add finalizer when object is created @@ -398,7 +445,7 @@ func (r *PatternReconciler) finalizeObject(instance *api.Pattern) error { return nil } - targetApp := newApplication(*qualifiedInstance) + targetApp := newApplication(*qualifiedInstance, nil) _ = controllerutil.SetOwnerReference(qualifiedInstance, targetApp, r.Scheme) _, app := getApplication(r.argoClient, applicationName(*qualifiedInstance)) diff --git a/controllers/subscription.go b/controllers/subscription.go index b45bdc2f5..a2b99f369 100644 --- a/controllers/subscription.go +++ b/controllers/subscription.go @@ -20,15 +20,21 @@ import ( "context" "fmt" "log" + "os" "reflect" + "io/ioutil" + "path/filepath" + + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + operatorv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" olmclient "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" api "github.com/hybrid-cloud-patterns/patterns-operator/api/v1alpha1" - operatorv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" ) const ( @@ -37,6 +43,16 @@ const ( ) func newSubscription(p api.Pattern) *operatorv1alpha1.Subscription { + return namedSubscription("openshift-gitops-operator", + subscriptionNamespace, + p.Spec.GitOpsConfig.OperatorChannel, + fmt.Sprintf("%s-%s", p.Name, p.Spec.ClusterGroupName), // eg. multicloud-gitops-hub + p.Spec.GitOpsConfig.OperatorCSV, + p.Spec.GitOpsConfig.UseCSV, + p.Spec.GitOpsConfig.ManualApproval) +} + +func namedSubscription(name, namespace, channel, owner, operatorCSV string, useCSV, manualApproval bool) *operatorv1alpha1.Subscription { // apiVersion: operators.coreos.com/v1alpha1 // kind: Subscription // metadata: @@ -57,7 +73,7 @@ func newSubscription(p api.Pattern) *operatorv1alpha1.Subscription { spec := &operatorv1alpha1.SubscriptionSpec{ CatalogSource: "redhat-operators", CatalogSourceNamespace: "openshift-marketplace", - Channel: p.Spec.GitOpsConfig.OperatorChannel, + Channel: channel, Package: "openshift-gitops-operator", InstallPlanApproval: operatorv1alpha1.ApprovalAutomatic, Config: &operatorv1alpha1.SubscriptionConfig{ @@ -70,18 +86,20 @@ func newSubscription(p api.Pattern) *operatorv1alpha1.Subscription { }, } - if p.Spec.GitOpsConfig.UseCSV { - spec.StartingCSV = p.Spec.GitOpsConfig.OperatorCSV + if useCSV { + spec.StartingCSV = operatorCSV } - if p.Spec.GitOpsConfig.ManualApproval { + if manualApproval { spec.InstallPlanApproval = operatorv1alpha1.ApprovalManual } + labels := map[string]string{"app.kubernetes.io/instance": owner} return &operatorv1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ - Name: "openshift-gitops-operator", - Namespace: subscriptionNamespace, + Name: name, + Namespace: namespace, + Labels: labels, }, Spec: spec, } @@ -89,7 +107,7 @@ func newSubscription(p api.Pattern) *operatorv1alpha1.Subscription { func getSubscription(client olmclient.Interface, name, namespace string) (error, *operatorv1alpha1.Subscription) { - sub, err := client.OperatorsV1alpha1().Subscriptions(subscriptionNamespace).Get(context.Background(), name, metav1.GetOptions{}) + sub, err := client.OperatorsV1alpha1().Subscriptions(namespace).Get(context.Background(), name, metav1.GetOptions{}) if err != nil { return err, nil } @@ -97,7 +115,7 @@ func getSubscription(client olmclient.Interface, name, namespace string) (error, } func createSubscription(client olmclient.Interface, sub *operatorv1alpha1.Subscription) error { - _, err := client.OperatorsV1alpha1().Subscriptions(subscriptionNamespace).Create(context.Background(), sub, metav1.CreateOptions{}) + _, err := client.OperatorsV1alpha1().Subscriptions(sub.Namespace).Create(context.Background(), sub, metav1.CreateOptions{}) return err } @@ -145,3 +163,76 @@ func updateSubscription(client olmclient.Interface, target, current *operatorv1a return nil, changed } + +func buildLiveSubscriptionExclusions(p api.Pattern, valueFiles []string, client olmclient.Interface) []string { + disabled := []string{} + + selfOwner := fmt.Sprintf("%s-%s", p.Name, p.Spec.ClusterGroupName) + ownerKey := "app.kubernetes.io/instance" + + for _, f := range valueFiles { + valuesFile := filepath.Join(p.Status.Path, f) + if _, err := os.Stat(valuesFile); !os.IsNotExist(err) { + + parsedSubs := parseValueSubs(valuesFile) + for key := range parsedSubs { + // handle arrays too + patternSub := parsedSubs[key].(map[interface{}]interface{}) + namespace := "openshift-operators" + if patternSub["namespace"] != nil { + namespace = patternSub["namespace"].(string) + } + + _, liveSub := getSubscription(client, patternSub["name"].(string), namespace) + if liveSub == nil { + continue + } + log.Println("Checking ownership", liveSub) + // labels: + // app.kubernetes.io/instance: multicloud-gitops-hub + val, ok := liveSub.Labels[ownerKey] + if !ok || val != selfOwner { + disabled = append(disabled, fmt.Sprintf("clusterGroup.subscriptions.%s.disabled", key)) + } + } + } + } + return disabled +} + +func parseValueSubs(filename string) map[interface{}]interface{} { + + //log.Println("Reading", filename) + yamlFile, err := ioutil.ReadFile(filename) + + if err != nil { + panic(err) + } + + var jsonconfig map[string]interface{} + + err = yaml.Unmarshal(yamlFile, &jsonconfig) + if err != nil { + panic(err) + } + + if jsonconfig["clusterGroup"] == nil { + return make(map[interface{}]interface{}, 0) + } + // log.Println("Found CG", reflect.TypeOf(jsonconfig["clusterGroup"]), jsonconfig["clusterGroup"]) + + cg := jsonconfig["clusterGroup"].(map[interface{}]interface{}) + if cg["subscriptions"] == nil { + return make(map[interface{}]interface{}, 0) + } + + subs := cg["subscriptions"].(map[interface{}]interface{}) + //if substr, err := yaml.Marshal(subs); err != nil { + // log.Println("Found subs: ", substr) + // for key := range subs { + // log.Println("Found sub: ", key) + // } + //} + + return subs +} diff --git a/controllers/subscription_test.go b/controllers/subscription_test.go new file mode 100644 index 000000000..438da6511 --- /dev/null +++ b/controllers/subscription_test.go @@ -0,0 +1,149 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + + // "github.com/go-logr/logr" + api "github.com/hybrid-cloud-patterns/patterns-operator/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("pattern controller", func() { + + var _ = Context("reconciliation", func() { + var ( + p *api.Pattern + qp *api.Pattern + reconciler *PatternReconciler + ) + BeforeEach(func() { + name := "subsTest" + nsOperators := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + reconciler = newFakeReconciler(nsOperators, buildNamedPatternManifest(name)) + + By("obtaining the pattern object") + p = &api.Pattern{} + Expect(reconciler.Client.Get(context.Background(), types.NamespacedName{Name: name, Namespace: namespace}, p)).To(Succeed()) + + By("obtaining applying defaults") + var err error + err, qp = reconciler.applyDefaults(p) + Expect(err).NotTo(HaveOccurred()) + + By(fmt.Sprintf("cloning %s", qp.Spec.GitConfig.TargetRepo)) + Expect(cloneRepo(qp.Spec.GitConfig.TargetRepo, qp.Status.Path, "")).To(Succeed()) + + By(fmt.Sprintf("checking out %s", qp.Status.Path)) + Expect(checkoutRevision(qp.Status.Path, "", qp.Spec.GitConfig.TargetRevision)).To(Succeed()) + + }) + It("pattern into empty cluster", func() { + By("building the exclusion list") + valueFiles := newApplicationValueFiles(*qp) + paramList := newApplicationParameters(*qp, valueFiles, reconciler.olmClient) + fmt.Println(paramList) + + By("checking the parameter list") + Expect(paramList).To(HaveLen(10)) + }) + It("pattern into cluster with foreign ACM", func() { + By("creating an ACM subscription") + sub := namedSubscription("advanced-cluster-management", + "open-cluster-management", + "release-2.6", + "dummy-dummy", + "advanced-cluster-management.v2.6.1", + false, + false) + Expect(createSubscription(reconciler.olmClient, sub)).To(Succeed()) + + By("building the exclusion list") + valueFiles := newApplicationValueFiles(*qp) + paramList := newApplicationParameters(*qp, valueFiles, reconciler.olmClient) + fmt.Println(paramList) + + By("checking the parameter list") + Expect(paramList).To(HaveLen(11)) + Expect(paramList[10].Name).To(Equal("clusterGroup.subscriptions.acm.disabled")) + }) + It("pattern into cluster with DynamicSubscriptions disabled", func() { + By("creating an ACM subscription") + sub := namedSubscription("advanced-cluster-management", + "open-cluster-management", + "release-2.6", + "dummy-dummy", + "advanced-cluster-management.v2.6.1", + false, + false) + Expect(createSubscription(reconciler.olmClient, sub)).To(Succeed()) + + By("building the exclusion list") + qp.Spec.DynamicSubscriptions = false + valueFiles := newApplicationValueFiles(*qp) + paramList := newApplicationParameters(*qp, valueFiles, reconciler.olmClient) + fmt.Println(paramList) + + By("checking the parameter list") + Expect(paramList).To(HaveLen(10)) + }) + It("pattern into cluster with self-owned ACM", func() { + By("creating an owned ACM subscription") + sub := namedSubscription("advanced-cluster-management", + "open-cluster-management", + "release-2.6", + fmt.Sprintf("%s-%s", qp.Name, qp.Spec.ClusterGroupName), + "advanced-cluster-management.v2.6.1", + false, + false) + //Expect(controllerutil.SetOwnerReference(qp, sub, reconciler.Scheme)).To(Succeed()) + Expect(createSubscription(reconciler.olmClient, sub)).To(Succeed()) + + By("building the exclusion list") + valueFiles := newApplicationValueFiles(*qp) + paramList := newApplicationParameters(*qp, valueFiles, reconciler.olmClient) + fmt.Println(paramList) + + By("checking the parameter list") + Expect(paramList).To(HaveLen(10)) + }) + }) +}) + +func buildNamedPatternManifest(name string) *api.Pattern { + return &api.Pattern{ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Finalizers: []string{api.PatternFinalizer}, + }, + Spec: api.PatternSpec{ + ClusterGroupName: "hub", + DynamicSubscriptions: true, + GitConfig: api.GitConfig{ + TargetRevision: "main", + TargetRepo: "http://github.com/hybrid-cloud-patterns/multicloud-gitops", + }, + }, + } +}