diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8084ff74..9b7bd24f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -9,19 +9,20 @@ rules: - apiGroups: - "" resources: - - events - verbs: - - create - - patch -- apiGroups: - - "" - resources: + - configmaps - secrets - serviceaccounts verbs: - get - list - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - kustomize.toolkit.fluxcd.io resources: diff --git a/controllers/kustomization_controller.go b/controllers/kustomization_controller.go index 3a05106e..782c235c 100644 --- a/controllers/kustomization_controller.go +++ b/controllers/kustomization_controller.go @@ -64,7 +64,7 @@ import ( // +kubebuilder:rbac:groups=kustomize.toolkit.fluxcd.io,resources=kustomizations/finalizers,verbs=get;create;update;patch;delete // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets;gitrepositories,verbs=get;list;watch // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/status;gitrepositories/status,verbs=get -// +kubebuilder:rbac:groups="",resources=secrets;serviceaccounts,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=configmaps;secrets;serviceaccounts,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // KustomizationReconciler reconciles a Kustomization object diff --git a/controllers/kustomization_controller_gc_test.go b/controllers/kustomization_controller_gc_test.go index d5c8e477..4fcf846d 100644 --- a/controllers/kustomization_controller_gc_test.go +++ b/controllers/kustomization_controller_gc_test.go @@ -25,6 +25,7 @@ import ( sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -111,7 +112,7 @@ var _ = Describe("KustomizationReconciler", func() { Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) }) - It("garbage collects removed workloads", func() { + It("garbage collects deleted manifests", func() { configMapManifest := func(name string) string { return fmt.Sprintf(`--- apiVersion: v1 @@ -167,9 +168,171 @@ data: Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed()) Eventually(func() bool { - err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "second", Namespace: namespace.Name}, &configMap) + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: kustomization.Name, Namespace: namespace.Name}, kustomization) return apierrors.IsNotFound(err) }, timeout, time.Second).Should(BeTrue()) + + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "second", Namespace: namespace.Name}, &configMap) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + + It("skips deleted manifests labeled with prune disabled", func() { + configMapManifest := func(name string, skip string) string { + return fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s + labels: + kustomize.toolkit.fluxcd.io/prune: "%[2]s" +data: + value: %[1]s +`, name, skip) + } + manifest := testserver.File{Name: "configmap.yaml", Body: configMapManifest("first", "disabled")} + artifact, err := artifactServer.ArtifactFromFiles([]testserver.File{manifest}) + Expect(err).ToNot(HaveOccurred()) + artifactURL, err := artifactServer.URLForFile(artifact) + Expect(err).ToNot(HaveOccurred()) + + gitRepo.Status.Artifact.URL = artifactURL + gitRepo.Status.Artifact.Revision = "first" + + Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed()) + Expect(k8sClient.Status().Update(context.Background(), gitRepo)).To(Succeed()) + Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + var got kustomizev1.Kustomization + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), ObjectKey(kustomization), &got) + c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition) + return c != nil && c.Reason == meta.ReconciliationSucceededReason + }, timeout, time.Second).Should(BeTrue()) + + var configMap corev1.ConfigMap + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap)).To(Succeed()) + Expect(configMap.Annotations[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).NotTo(BeEmpty()) + + manifest.Body = configMapManifest("second", "enabled") + artifact, err = artifactServer.ArtifactFromFiles([]testserver.File{manifest}) + Expect(err).ToNot(HaveOccurred()) + artifactURL, err = artifactServer.URLForFile(artifact) + Expect(err).ToNot(HaveOccurred()) + + gitRepo.Status.Artifact.URL = artifactURL + gitRepo.Status.Artifact.Revision = "second" + Expect(k8sClient.Status().Update(context.Background(), gitRepo)).To(Succeed()) + + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), ObjectKey(kustomization), &got) + return got.Status.LastAppliedRevision == gitRepo.Status.Artifact.Revision + }, timeout, time.Second).Should(BeTrue()) + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap) + Expect(err).ToNot(HaveOccurred()) + + Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed()) + Eventually(func() bool { + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: kustomization.Name, Namespace: namespace.Name}, kustomization) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "second", Namespace: namespace.Name}, &configMap) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap) + Expect(err).ToNot(HaveOccurred()) + }) + + It("does not set the checksum annotation when GC is disabled", func() { + deploymentManifest := func(namespace string) string { + return fmt.Sprintf(`--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment + namespace: %s +spec: + selector: + matchLabels: + app: test-deployment + template: + metadata: + labels: + app: test-deployment + spec: + containers: + - name: test + image: podinfo +`, + namespace) + } + + manifests := []testserver.File{ + { + Name: "deployment.yaml", + Body: deploymentManifest(namespace.Name), + }, + } + artifact, err := artifactServer.ArtifactFromFiles(manifests) + Expect(err).NotTo(HaveOccurred()) + + url := fmt.Sprintf("%s/%s", artifactServer.URL(), artifact) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("%s", randStringRunes(5)), + Namespace: namespace.Name, + } + repository := readyGitRepository(repositoryName, url, "v1", "") + Expect(k8sClient.Create(context.Background(), repository)).To(Succeed()) + Expect(k8sClient.Status().Update(context.Background(), repository)).To(Succeed()) + defer k8sClient.Delete(context.Background(), repository) + + kName := types.NamespacedName{ + Name: fmt.Sprintf("%s", randStringRunes(5)), + Namespace: namespace.Name, + } + k := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kName.Name, + Namespace: kName.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + KubeConfig: kubeconfig, + Interval: metav1.Duration{Duration: time.Hour}, + Path: "./", + Prune: false, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: repository.Name, + }, + Suspend: false, + Timeout: &metav1.Duration{Duration: 60 * time.Second}, + Validation: "client", + Force: true, + }, + } + Expect(k8sClient.Create(context.Background(), k)).To(Succeed()) + defer k8sClient.Delete(context.Background(), k) + + got := &kustomizev1.Kustomization{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), kName, got) + c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition) + return c != nil && c.Reason == meta.ReconciliationSucceededReason + }, timeout, time.Second).Should(BeTrue()) + Expect(got.Status.LastAppliedRevision).To(Equal("v1")) + + deployment := &appsv1.Deployment{} + deploymentName := types.NamespacedName{Name: "test-deployment", Namespace: namespace.Name} + Expect(k8sClient.Get(context.Background(), deploymentName, deployment)).To(Succeed()) + Expect(deployment.Annotations[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).To(BeEmpty()) + + Expect(k8sClient.Delete(context.Background(), k)).To(Succeed()) + Eventually(func() bool { + err = k8sClient.Get(context.Background(), kName, got) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + + Expect(k8sClient.Get(context.Background(), deploymentName, deployment)).To(Succeed()) }) }) }) diff --git a/controllers/kustomization_controller_test.go b/controllers/kustomization_controller_test.go index 16ee6dd7..4f48cd6b 100644 --- a/controllers/kustomization_controller_test.go +++ b/controllers/kustomization_controller_test.go @@ -360,92 +360,6 @@ spec: Expect(deployment.Spec.Selector.MatchLabels["app"]).To(Equal("v2")) }) }) - - Describe("Kustomization resource annotation", func() { - deploymentManifest := func(namespace string) string { - return fmt.Sprintf(`--- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: test-deployment - namespace: %s -spec: - selector: - matchLabels: - app: test-deployment - template: - metadata: - labels: - app: test-deployment - spec: - containers: - - name: test - image: podinfo -`, - namespace) - } - - It("should have no annotation when if prune is false", func() { - manifests := []testserver.File{ - { - Name: "deployment.yaml", - Body: deploymentManifest(namespace.Name), - }, - } - artifact, err := httpServer.ArtifactFromFiles(manifests) - Expect(err).NotTo(HaveOccurred()) - - url := fmt.Sprintf("%s/%s", httpServer.URL(), artifact) - - repositoryName := types.NamespacedName{ - Name: fmt.Sprintf("%s", randStringRunes(5)), - Namespace: namespace.Name, - } - repository := readyGitRepository(repositoryName, url, "v1", "") - Expect(k8sClient.Create(context.Background(), repository)).To(Succeed()) - Expect(k8sClient.Status().Update(context.Background(), repository)).To(Succeed()) - defer k8sClient.Delete(context.Background(), repository) - - kName := types.NamespacedName{ - Name: fmt.Sprintf("%s", randStringRunes(5)), - Namespace: namespace.Name, - } - k := &kustomizev1.Kustomization{ - ObjectMeta: metav1.ObjectMeta{ - Name: kName.Name, - Namespace: kName.Namespace, - }, - Spec: kustomizev1.KustomizationSpec{ - KubeConfig: kubeconfig, - Interval: metav1.Duration{Duration: reconciliationInterval}, - Path: "./", - Prune: false, - SourceRef: kustomizev1.CrossNamespaceSourceReference{ - Kind: sourcev1.GitRepositoryKind, - Name: repository.Name, - }, - Suspend: false, - Timeout: &metav1.Duration{Duration: 60 * time.Second}, - Validation: "client", - Force: true, - }, - } - Expect(k8sClient.Create(context.Background(), k)).To(Succeed()) - defer k8sClient.Delete(context.Background(), k) - - got := &kustomizev1.Kustomization{} - Eventually(func() bool { - _ = k8sClient.Get(context.Background(), kName, got) - c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition) - return c != nil && c.Reason == meta.ReconciliationSucceededReason - }, timeout, interval).Should(BeTrue()) - Expect(got.Status.LastAppliedRevision).To(Equal("v1")) - - deployment := &appsv1.Deployment{} - Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "test-deployment", Namespace: namespace.Name}, deployment)).To(Succeed()) - Expect(deployment.Annotations[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).To(BeEmpty()) - }) - }) }) }) diff --git a/controllers/kustomization_gc.go b/controllers/kustomization_gc.go index b575265b..7cbfd9a8 100644 --- a/controllers/kustomization_gc.go +++ b/controllers/kustomization_gc.go @@ -137,9 +137,8 @@ func (kgc *KustomizeGarbageCollector) Prune(timeout time.Duration, name string, return changeSet, true } -// Check both labels and annotations for the checksum to preserve backwards compatibility +// Determine staleness by checking if the annotation matches the latest checksum func (kgc *KustomizeGarbageCollector) isStale(obj unstructured.Unstructured) bool { - itemLabelChecksum := obj.GetLabels()[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)] itemAnnotationChecksum := obj.GetAnnotations()[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)] switch kgc.newChecksum { @@ -147,8 +146,6 @@ func (kgc *KustomizeGarbageCollector) isStale(obj unstructured.Unstructured) boo return true case itemAnnotationChecksum: return false - case itemLabelChecksum: - return false default: return true } diff --git a/docs/spec/v1beta1/kustomization.md b/docs/spec/v1beta1/kustomization.md index 3e58ed1e..c842b759 100644 --- a/docs/spec/v1beta1/kustomization.md +++ b/docs/spec/v1beta1/kustomization.md @@ -364,18 +364,19 @@ but are missing from the current source revision, are removed from cluster autom Garbage collection is also performed when a Kustomization object is deleted, triggering a removal of all Kubernetes objects previously applied on the cluster. -To keep track of the Kubernetes objects reconciled from a Kustomization, the following labels -are injected into the manifests: +To keep track of the Kubernetes objects reconciled from a Kustomization, the following metadata +is injected into the manifests: ```yaml labels: kustomize.toolkit.fluxcd.io/name: "" kustomize.toolkit.fluxcd.io/namespace: "" +annotations: kustomize.toolkit.fluxcd.io/checksum: "" ``` -The checksum label value is updated if the content of `spec.path` changes. -When pruning is disabled, the checksum label is omitted. +The checksum annotation value is updated if the content of `spec.path` changes. +When pruning is disabled, the checksum annotation is omitted. You can disable pruning for certain resources by either labeling or annotating them with: