From d4a175c7c29d95079a8447ab479eaeeb1a52b984 Mon Sep 17 00:00:00 2001 From: Patrick Seidensal Date: Thu, 14 Nov 2024 22:08:47 +0100 Subject: [PATCH 1/3] Import v1alpha1 package as fleet --- .../gitops/reconciler/status_controller.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/cmd/controller/gitops/reconciler/status_controller.go b/internal/cmd/controller/gitops/reconciler/status_controller.go index 7aa990fa67..8b5a790175 100644 --- a/internal/cmd/controller/gitops/reconciler/status_controller.go +++ b/internal/cmd/controller/gitops/reconciler/status_controller.go @@ -8,7 +8,7 @@ import ( "github.com/rancher/fleet/internal/cmd/controller/status" "github.com/rancher/fleet/internal/cmd/controller/summary" "github.com/rancher/fleet/internal/resourcestatus" - v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" + fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" "github.com/rancher/fleet/pkg/durations" "github.com/rancher/fleet/pkg/sharding" @@ -32,12 +32,12 @@ type StatusReconciler struct { func (r *StatusReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.GitRepo{}). + For(&fleet.GitRepo{}). Watches( // Fan out from bundle to gitrepo - &v1alpha1.Bundle{}, + &fleet.Bundle{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []ctrl.Request { - repo := a.GetLabels()[v1alpha1.RepoLabel] + repo := a.GetLabels()[fleet.RepoLabel] if repo != "" { return []ctrl.Request{{ NamespacedName: types.NamespacedName{ @@ -62,7 +62,7 @@ func (r *StatusReconciler) SetupWithManager(mgr ctrl.Manager) error { // display information to the user. func (r *StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx).WithName("gitops-status") - gitrepo := &v1alpha1.GitRepo{} + gitrepo := &fleet.GitRepo{} if err := r.Get(ctx, req.NamespacedName, gitrepo); err != nil && !errors.IsNotFound(err) { return ctrl.Result{}, err @@ -91,10 +91,10 @@ func (r *StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr logger.V(1).Info("Reconciling GitRepo status") - bdList := &v1alpha1.BundleDeploymentList{} + bdList := &fleet.BundleDeploymentList{} err := r.List(ctx, bdList, client.MatchingLabels{ - v1alpha1.RepoLabel: gitrepo.Name, - v1alpha1.BundleNamespaceLabel: gitrepo.Namespace, + fleet.RepoLabel: gitrepo.Name, + fleet.BundleNamespaceLabel: gitrepo.Namespace, }) if err != nil { return ctrl.Result{}, err @@ -118,7 +118,7 @@ func (r *StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, nil } -func setStatus(list *v1alpha1.BundleDeploymentList, gitrepo *v1alpha1.GitRepo) error { +func setStatus(list *fleet.BundleDeploymentList, gitrepo *fleet.GitRepo) error { // sort for resourceKey? sort.Slice(list.Items, func(i, j int) bool { return list.Items[i].UID < list.Items[j].UID From ed6552ba5ce3d1ec02c909991018c8dbf292bb9c Mon Sep 17 00:00:00 2001 From: Patrick Seidensal Date: Thu, 14 Nov 2024 14:06:19 +0100 Subject: [PATCH 2/3] Show bundle errors in Bundle and GitRepo Refers to #2943 --- .../gitops/reconciler/status_controller.go | 82 ++++++++++++++ .../reconciler/bundle_controller.go | 101 +++++++++--------- 2 files changed, 131 insertions(+), 52 deletions(-) diff --git a/internal/cmd/controller/gitops/reconciler/status_controller.go b/internal/cmd/controller/gitops/reconciler/status_controller.go index 8b5a790175..e99972a653 100644 --- a/internal/cmd/controller/gitops/reconciler/status_controller.go +++ b/internal/cmd/controller/gitops/reconciler/status_controller.go @@ -11,7 +11,9 @@ import ( fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" "github.com/rancher/fleet/pkg/durations" "github.com/rancher/fleet/pkg/sharding" + "github.com/rancher/wrangler/v3/pkg/genericcondition" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -109,6 +111,29 @@ func (r *StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr gitrepo.Status.Display.State = "GitUpdating" } + // We're explicitly setting the ready status from a bundle here, but only if it isn't ready. + // + // - If the bundle has no deployments, there is no status to be copied from the setStatus + // function, so that we won't overwrite anything. + // + // - If the bundle has rendering issues and there are deployments of which there is at least one + // in a failed state, the status of the bundle deployments would be overwritten by the bundle + // status. + // + // - If the bundle has no rendering issues but there are deployments in a failed state, the code + // will overwrite the gitrepo's ready status condition with the ready status condition coming + // from the bundle. Because both have the same content, we can unconditionally set the status + // from the bundle. + // + // So we're basically just making sure the status from the bundle is being set on the gitrepo, + // even if there are no bundle deployments, which is the case for issues with rendering the + // manifests, for instance. In that case no bundle deployments are created, but an error is set + // in a ready status condition on the bundle. + err = r.setReadyStatusFromBundle(ctx, gitrepo) + if err != nil { + return ctrl.Result{}, err + } + err = r.Client.Status().Update(ctx, gitrepo) if err != nil { logger.Error(err, "Reconcile failed update to git repo status", "status", gitrepo.Status) @@ -139,3 +164,60 @@ func setStatus(list *fleet.BundleDeploymentList, gitrepo *fleet.GitRepo) error { return nil } + +// setReadyStatusFromBundle fetches all bundles from a given gitrepo, checks the ready status conditions +// from the bundles and applies one on the gitrepo if it isn't ready. The purpose is to make +// rendering issues visible in the gitrepo status. Those issues need to be made explicitly visible +// since the other statuses are calculated from bundle deployments, which do not exist when +// rendering manifests fail. Should an issue be on the bundle, it will be copied to the gitrepo. +func (r StatusReconciler) setReadyStatusFromBundle(ctx context.Context, gitrepo *fleet.GitRepo) error { + bList := &fleet.BundleList{} + err := r.List(ctx, bList, client.MatchingLabels{ + fleet.RepoLabel: gitrepo.Name, + }, client.InNamespace(gitrepo.Namespace)) + if err != nil { + return err + } + + found := false + // Find a ready status condition in a bundle which is not ready. + var condition genericcondition.GenericCondition +bundles: + for _, bundle := range bList.Items { + if bundle.Status.Conditions == nil { + continue + } + + for _, c := range bundle.Status.Conditions { + if c.Type == string(fleet.Ready) && c.Status == v1.ConditionFalse { + condition = c + found = true + break bundles + } + } + } + + // No ready condition found in any bundle, nothing to do here. + if !found { + return nil + } + + found = false + newConditions := make([]genericcondition.GenericCondition, 0, len(gitrepo.Status.Conditions)) + for _, c := range gitrepo.Status.Conditions { + if c.Type == string(fleet.Ready) { + // Replace the ready condition with the one from the bundle + newConditions = append(newConditions, condition) + found = true + continue + } + newConditions = append(newConditions, c) + } + if !found { + // Add the ready condition from the bundle to the gitrepo. + newConditions = append(newConditions, condition) + } + gitrepo.Status.Conditions = newConditions + + return nil +} diff --git a/internal/cmd/controller/reconciler/bundle_controller.go b/internal/cmd/controller/reconciler/bundle_controller.go index 3d0b6dad94..f107211842 100644 --- a/internal/cmd/controller/reconciler/bundle_controller.go +++ b/internal/cmd/controller/reconciler/bundle_controller.go @@ -7,8 +7,10 @@ import ( "fmt" "os" "strconv" + "time" "github.com/go-logr/logr" + "github.com/rancher/fleet/internal/cmd/controller/finalize" "github.com/rancher/fleet/internal/cmd/controller/summary" "github.com/rancher/fleet/internal/cmd/controller/target" @@ -17,13 +19,14 @@ import ( "github.com/rancher/fleet/internal/ociwrapper" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" "github.com/rancher/fleet/pkg/sharding" + "github.com/rancher/wrangler/v3/pkg/genericcondition" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" apierrors "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/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -132,6 +135,7 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if err := r.Get(ctx, req.NamespacedName, bundle); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } + bundleOrig := bundle.DeepCopy() if bundle.Labels[fleet.RepoLabel] != "" { logger = logger.WithValues( @@ -184,6 +188,18 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr matchedTargets, err := r.Builder.Targets(ctx, bundle, manifestID) if err != nil { + // When targeting fails, we don't want to continue and we make the error message visible in + // the UI. For that we use a status condition of type Ready. + bundle.Status.Conditions = []genericcondition.GenericCondition{ + { + Type: string(fleet.Ready), + Status: v1.ConditionFalse, + Message: "Targeting error: " + err.Error(), + LastUpdateTime: metav1.Now().UTC().Format(time.RFC3339), + }, + } + + err := r.updateStatus(ctx, bundleOrig, bundle) return ctrl.Result{}, err } @@ -226,9 +242,8 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr summary.SetReadyConditions(&bundle.Status, "Cluster", bundle.Status.Summary) bundle.Status.ObservedGeneration = bundle.Generation - // build BundleDeployments out of targets discarding Status, replacing - // DependsOn with the bundle's DependsOn (pure function) and replacing - // the labels with the bundle's labels + // build BundleDeployments out of targets discarding Status, replacing DependsOn with the + // bundle's DependsOn (pure function) and replacing the labels with the bundle's labels for _, target := range matchedTargets { bd, err := r.createBundleDeployment( ctx, @@ -247,20 +262,7 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } updateDisplay(&bundle.Status) - - err = retry.RetryOnConflict(retry.DefaultRetry, func() error { - t := &fleet.Bundle{} - if err := r.Get(ctx, req.NamespacedName, t); err != nil { - return err - } - t.Status = bundle.Status - return r.Status().Update(ctx, t) - }) - if err != nil { - logger.V(1).Info("Reconcile failed final update to bundle status", "status", bundle.Status, "error", err) - } else { - metrics.BundleCollector.Collect(ctx, bundle) - } + err = r.updateStatus(ctx, bundleOrig, bundle) return ctrl.Result{}, err } @@ -297,16 +299,8 @@ func (r *BundleReconciler) addOrRemoveFinalizer(ctx context.Context, logger logr return true, client.IgnoreNotFound(err) } - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := r.Get(ctx, req.NamespacedName, bundle); err != nil { - return err - } - - controllerutil.RemoveFinalizer(bundle, bundleFinalizer) - - return r.Update(ctx, bundle) - }) - + controllerutil.RemoveFinalizer(bundle, bundleFinalizer) + err := r.Update(ctx, bundle) if client.IgnoreNotFound(err) != nil { return true, err } @@ -316,16 +310,8 @@ func (r *BundleReconciler) addOrRemoveFinalizer(ctx context.Context, logger logr } if !controllerutil.ContainsFinalizer(bundle, bundleFinalizer) { - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := r.Get(ctx, req.NamespacedName, bundle); err != nil { - return err - } - - controllerutil.AddFinalizer(bundle, bundleFinalizer) - - return r.Update(ctx, bundle) - }) - + controllerutil.AddFinalizer(bundle, bundleFinalizer) + err := r.Update(ctx, bundle) if client.IgnoreNotFound(err) != nil { return true, err } @@ -366,24 +352,20 @@ func (r *BundleReconciler) createBundleDeployment( bd.Spec.OCIContents = contentsInOCI bd.Spec.HelmChartOptions = helmAppOptions - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if contentsInOCI { - return nil // no contents resources stored in etcd, no finalizers to add here. - } - + // contents resources stored in etcd, finalizers to add here. + if !contentsInOCI { content := &fleet.Content{} - if err := r.Get(ctx, types.NamespacedName{Name: manifestID}, content); err != nil { - return client.IgnoreNotFound(err) + err := r.Get(ctx, types.NamespacedName{Name: manifestID}, content) + if client.IgnoreNotFound(err) != nil { + logger.Error(err, "Reconcile failed to get content", "content ID", manifestID) + return nil, err } - if added := controllerutil.AddFinalizer(content, bd.Name); !added { - return nil + if added := controllerutil.AddFinalizer(content, bd.Name); added { + if err := r.Update(ctx, content); err != nil { + logger.Error(err, "Reconcile failed to add content finalizer", "content ID", manifestID) + } } - - return r.Update(ctx, content) - }) - if err != nil { - logger.Error(err, "Reconcile failed to add content finalizer", "content ID", manifestID) } contentsInHelmChart := helmAppOptions != nil @@ -503,3 +485,18 @@ func experimentalHelmOpsEnabled() bool { value, err := strconv.ParseBool(os.Getenv("EXPERIMENTAL_HELM_OPS")) return err == nil && value } + +// updateStatus patches the status of the bundle and collects metrics upon a successful update of +// the bundle status. It returns nil if the status update is successful, otherwise it returns an +// error. +func (r *BundleReconciler) updateStatus(ctx context.Context, orig *fleet.Bundle, bundle *fleet.Bundle) error { + logger := log.FromContext(ctx).WithName("bundle - updateStatus") + statusPatch := client.MergeFrom(orig) + err := r.Status().Patch(ctx, bundle, statusPatch) + if err != nil { + logger.V(1).Info("Reconcile failed update to bundle status", "status", bundle.Status, "error", err) + return err + } + metrics.BundleCollector.Collect(ctx, bundle) + return nil +} From 4e58cabda729b54f7f001463672d34f793dd8a32 Mon Sep 17 00:00:00 2001 From: Patrick Seidensal Date: Mon, 2 Dec 2024 09:31:29 +0100 Subject: [PATCH 3/3] Add E2E tests Refers to #2943 --- .../chart-with-template-vars/Chart.yaml | 24 +++ .../chart-with-template-vars/fleet.yaml | 4 + .../templates/configmap.yaml | 7 + e2e/assets/status/gitrepo.yaml | 11 ++ e2e/single-cluster/gitrepo_test.go | 8 +- e2e/single-cluster/status_test.go | 168 ++++++++++++++++++ 6 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 e2e/assets/status/chart-with-template-vars/Chart.yaml create mode 100644 e2e/assets/status/chart-with-template-vars/fleet.yaml create mode 100644 e2e/assets/status/chart-with-template-vars/templates/configmap.yaml create mode 100644 e2e/assets/status/gitrepo.yaml diff --git a/e2e/assets/status/chart-with-template-vars/Chart.yaml b/e2e/assets/status/chart-with-template-vars/Chart.yaml new file mode 100644 index 0000000000..017949df22 --- /dev/null +++ b/e2e/assets/status/chart-with-template-vars/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: chart-with-template-vars +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/e2e/assets/status/chart-with-template-vars/fleet.yaml b/e2e/assets/status/chart-with-template-vars/fleet.yaml new file mode 100644 index 0000000000..01e3b9bbaf --- /dev/null +++ b/e2e/assets/status/chart-with-template-vars/fleet.yaml @@ -0,0 +1,4 @@ +helm: + values: + templatedLabel: "${ .ClusterLabels.foo }-foo" + releaseName: reproducer diff --git a/e2e/assets/status/chart-with-template-vars/templates/configmap.yaml b/e2e/assets/status/chart-with-template-vars/templates/configmap.yaml new file mode 100644 index 0000000000..2327303f39 --- /dev/null +++ b/e2e/assets/status/chart-with-template-vars/templates/configmap.yaml @@ -0,0 +1,7 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: chart-with-template-vars-configmap + namespace: fleet-local +data: + foo: bar diff --git a/e2e/assets/status/gitrepo.yaml b/e2e/assets/status/gitrepo.yaml new file mode 100644 index 0000000000..c34afdb185 --- /dev/null +++ b/e2e/assets/status/gitrepo.yaml @@ -0,0 +1,11 @@ +kind: GitRepo +apiVersion: fleet.cattle.io/v1alpha1 +metadata: + name: {{.Name}} + namespace: fleet-local +spec: + repo: {{.Repo}} + branch: {{.Branch}} + targetNamespace: {{.TargetNamespace}} + paths: + - examples diff --git a/e2e/single-cluster/gitrepo_test.go b/e2e/single-cluster/gitrepo_test.go index 6989a2d834..0ac10257c7 100644 --- a/e2e/single-cluster/gitrepo_test.go +++ b/e2e/single-cluster/gitrepo_test.go @@ -194,7 +194,7 @@ var _ = Describe("Monitoring Git repos via HTTP for change", Label("infra-setup" }, } Eventually(func(g Gomega) { - status := getGitRepoStatus(k, gitrepoName) + status := getGitRepoStatus(g, k, gitrepoName) g.Expect(status).To(matchGitRepoStatus(expectedStatus)) }).Should(Succeed()) @@ -355,7 +355,7 @@ var _ = Describe("Monitoring Git repos via HTTP for change", Label("infra-setup" }, } Eventually(func(g Gomega) { - status := getGitRepoStatus(k, gitrepoName) + status := getGitRepoStatus(g, k, gitrepoName) g.Expect(status).To(matchGitRepoStatus(expectedStatus)) }).Should(Succeed()) @@ -381,10 +381,10 @@ func replace(path string, s string, r string) { } // getGitRepoStatus retrieves the status of the gitrepo with the provided name. -func getGitRepoStatus(k kubectl.Command, name string) fleet.GitRepoStatus { +func getGitRepoStatus(g Gomega, k kubectl.Command, name string) fleet.GitRepoStatus { gr, err := k.Get("gitrepo", name, "-o=json") - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) var gitrepo fleet.GitRepo _ = json.Unmarshal([]byte(gr), &gitrepo) diff --git a/e2e/single-cluster/status_test.go b/e2e/single-cluster/status_test.go index 13d60e44dd..fbc8bbf06e 100644 --- a/e2e/single-cluster/status_test.go +++ b/e2e/single-cluster/status_test.go @@ -1,13 +1,23 @@ package singlecluster_test import ( + "encoding/json" "errors" + "fmt" + "math/rand" + "os" + "path" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/rancher/fleet/e2e/testenv" + "github.com/rancher/fleet/e2e/testenv/githelper" "github.com/rancher/fleet/e2e/testenv/kubectl" + fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" + "github.com/rancher/wrangler/v3/pkg/genericcondition" + corev1 "k8s.io/api/core/v1" ) var _ = Describe("Checks status updates happen for a simple deployment", Ordered, func() { @@ -108,3 +118,161 @@ var _ = Describe("Checks status updates happen for a simple deployment", Ordered }) }) }) + +var _ = Describe("Checks that template errors are shown in bundles and gitrepos", Ordered, Label("infra-setup"), func() { + var ( + tmpDir string + cloneDir string + k kubectl.Command + gh *githelper.Git + repoName string + inClusterRepoURL string + gitrepoName string + r = rand.New(rand.NewSource(GinkgoRandomSeed())) + targetNamespace string + ) + + BeforeEach(func() { + k = env.Kubectl.Namespace(env.Namespace) + repoName = "repo" + }) + + JustBeforeEach(func() { + // Build git repo URL reachable _within_ the cluster, for the GitRepo + host, err := githelper.BuildGitHostname(env.Namespace) + Expect(err).ToNot(HaveOccurred()) + + addr, err := githelper.GetExternalRepoAddr(env, port, repoName) + Expect(err).ToNot(HaveOccurred()) + gh = githelper.NewHTTP(addr) + + inClusterRepoURL = gh.GetInClusterURL(host, port, repoName) + + tmpDir, _ = os.MkdirTemp("", "fleet-") + cloneDir = path.Join(tmpDir, repoName) + + gitrepoName = testenv.RandomFilename("status-test", r) + + _, err = gh.Create(cloneDir, testenv.AssetPath("status/chart-with-template-vars"), "examples") + Expect(err).ToNot(HaveOccurred()) + + err = testenv.ApplyTemplate(k, testenv.AssetPath("status/gitrepo.yaml"), struct { + Name string + Repo string + Branch string + TargetNamespace string + }{ + gitrepoName, + inClusterRepoURL, + gh.Branch, + targetNamespace, // to avoid conflicts with other tests + }) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + _ = os.RemoveAll(tmpDir) + + _, err := k.Delete("gitrepo", gitrepoName) + Expect(err).ToNot(HaveOccurred()) + + // Check that the bundle deployment resource has been deleted + Eventually(func(g Gomega) { + out, _ := k.Get( + "bundledeployments", + "-A", + "-l", + fmt.Sprintf("fleet.cattle.io/repo-name=%s", gitrepoName), + ) + g.Expect(out).To(ContainSubstring("No resources found")) + }).Should(Succeed()) + + // Deleting the targetNamespace is not necessary when the GitRepo did not successfully + // render, as in a few test cases here. If no targetNamespace was created, trying to delete + // the namespace will result in an error, which is why we are not checking for errors when + // deleting namespaces here. + _, _ = k.Delete("ns", targetNamespace) + }) + + expectNoError := func(g Gomega, conditions []genericcondition.GenericCondition) { + for _, condition := range conditions { + if condition.Type == string(fleet.Ready) { + g.Expect(condition.Status).To(Equal(corev1.ConditionTrue)) + g.Expect(condition.Message).To(BeEmpty()) + break + } + } + } + + expectTargetingError := func(g Gomega, conditions []genericcondition.GenericCondition) { + found := false + for _, condition := range conditions { + if condition.Type == string(fleet.Ready) { + g.Expect(condition.Status).To(Equal(corev1.ConditionFalse)) + g.Expect(condition.Message).To(ContainSubstring("Targeting error")) + g.Expect(condition.Message).To( + ContainSubstring( + "<.ClusterLabels.foo>: map has no entry for key \"foo\"")) + found = true + break + } + } + g.Expect(found).To(BeTrue()) + } + + ensureClusterHasLabelFoo := func() (string, error) { + return k.Namespace("fleet-local"). + Patch("cluster", "local", "--type", "json", "--patch", + `[{"op": "add", "path": "/metadata/labels/foo", "value": "bar"}]`) + } + + ensureClusterHasNoLabelFoo := func() (string, error) { + return k.Namespace("fleet-local"). + Patch("cluster", "local", "--type", "json", "--patch", + `[{"op": "remove", "path": "/metadata/labels/foo"}]`) + } + + When("a git repository is created that contains a template error", func() { + BeforeEach(func() { + targetNamespace = testenv.NewNamespaceName("target", r) + }) + + It("should have an error in the bundle", func() { + _, _ = ensureClusterHasNoLabelFoo() + Eventually(func(g Gomega) { + status := getBundleStatus(g, k, gitrepoName+"-examples") + expectTargetingError(g, status.Conditions) + }).Should(Succeed()) + }) + + It("should have an error in the gitrepo", func() { + _, _ = ensureClusterHasNoLabelFoo() + Eventually(func(g Gomega) { + status := getGitRepoStatus(g, k, gitrepoName) + expectTargetingError(g, status.Conditions) + }).Should(Succeed()) + }) + }) + + When("a git repository is created that contains no template error", func() { + It("should have no error in the bundle", func() { + _, _ = ensureClusterHasLabelFoo() + Eventually(func(g Gomega) { + status := getBundleStatus(g, k, gitrepoName+"-examples") + expectNoError(g, status.Conditions) + }).Should(Succeed()) + }) + }) +}) + +// getBundleStatus retrieves the status of the bundle with the provided name. +func getBundleStatus(g Gomega, k kubectl.Command, name string) fleet.BundleStatus { + gr, err := k.Get("bundle", name, "-o=json") + + g.Expect(err).ToNot(HaveOccurred()) + + var bundle fleet.Bundle + _ = json.Unmarshal([]byte(gr), &bundle) + + return bundle.Status +}