diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adcb2825..ec03a314 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,7 @@ Prerequisites: * go >= 1.13 * kubebuilder >= 2.3 * kustomize >= 3.1 +* kubectl >= 1.21 You can run the unit tests by simply doing diff --git a/controllers/kustomization_controller_gc_test.go b/controllers/kustomization_controller_gc_test.go index e0d271e3..d5c8e477 100644 --- a/controllers/kustomization_controller_gc_test.go +++ b/controllers/kustomization_controller_gc_test.go @@ -144,7 +144,7 @@ data: var configMap corev1.ConfigMap Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap)).To(Succeed()) - Expect(configMap.Labels[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).NotTo(BeEmpty()) + Expect(configMap.Annotations[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).NotTo(BeEmpty()) manifest.Body = configMapManifest("second") artifact, err = artifactServer.ArtifactFromFiles([]testserver.File{manifest}) @@ -163,7 +163,7 @@ data: err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap) Expect(apierrors.IsNotFound(err)).To(BeTrue()) Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "second", Namespace: namespace.Name}, &configMap)).To(Succeed()) - Expect(configMap.Labels[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).NotTo(BeEmpty()) + Expect(configMap.Annotations[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).NotTo(BeEmpty()) Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed()) Eventually(func() bool { diff --git a/controllers/kustomization_controller_test.go b/controllers/kustomization_controller_test.go index 4f48cd6b..16ee6dd7 100644 --- a/controllers/kustomization_controller_test.go +++ b/controllers/kustomization_controller_test.go @@ -360,6 +360,92 @@ 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 b17d93e3..b575265b 100644 --- a/controllers/kustomization_gc.go +++ b/controllers/kustomization_gc.go @@ -137,9 +137,21 @@ func (kgc *KustomizeGarbageCollector) Prune(timeout time.Duration, name string, return changeSet, true } +// Check both labels and annotations for the checksum to preserve backwards compatibility func (kgc *KustomizeGarbageCollector) isStale(obj unstructured.Unstructured) bool { - itemChecksum := obj.GetLabels()[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)] - return kgc.newChecksum == "" || itemChecksum != kgc.newChecksum + itemLabelChecksum := obj.GetLabels()[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)] + itemAnnotationChecksum := obj.GetAnnotations()[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)] + + switch kgc.newChecksum { + case "": + return true + case itemAnnotationChecksum: + return false + case itemLabelChecksum: + return false + default: + return true + } } func (kgc *KustomizeGarbageCollector) shouldSkip(obj unstructured.Unstructured) bool { @@ -156,7 +168,11 @@ func gcLabels(name, namespace, checksum string) map[string]string { return map[string]string{ fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): name, fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): namespace, - fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group): checksum, + } +} +func gcAnnotation(checksum string) map[string]string { + return map[string]string{ + fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group): checksum, } } @@ -165,4 +181,5 @@ func selectorLabels(name, namespace string) map[string]string { fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): name, fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): namespace, } + } diff --git a/controllers/kustomization_generator.go b/controllers/kustomization_generator.go index 7c47114e..490b53f8 100644 --- a/controllers/kustomization_generator.go +++ b/controllers/kustomization_generator.go @@ -36,13 +36,13 @@ import ( kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/yaml" - "github.com/fluxcd/pkg/apis/kustomize" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1" + "github.com/fluxcd/pkg/apis/kustomize" ) const ( - transformerFileName = "kustomization-gc-labels.yaml" + transformerFileName = "kustomization-gc-labels.yaml" + transformerAnnotationFileName = "kustomization-gc-annotations.yaml" ) type KustomizeGenerator struct { @@ -68,6 +68,9 @@ func (kg *KustomizeGenerator) WriteFile(ctx context.Context, dirPath string) (st if err := kg.generateLabelTransformer(checksum, dirPath); err != nil { return "", err } + if err = kg.generateAnnotationTransformer(checksum, dirPath); err != nil { + return "", err + } data, err := ioutil.ReadFile(kfile) if err != nil { @@ -86,18 +89,16 @@ func (kg *KustomizeGenerator) WriteFile(ctx context.Context, dirPath string) (st } if len(kus.Transformers) == 0 { - kus.Transformers = []string{transformerFileName} + kus.Transformers = []string{transformerFileName, transformerAnnotationFileName} } else { - var exists bool - for _, transformer := range kus.Transformers { - if transformer == transformerFileName { - exists = true - break - } - } - if !exists { + if !find(kus.Transformers, transformerFileName) { kus.Transformers = append(kus.Transformers, transformerFileName) } + + if !find(kus.Transformers, transformerAnnotationFileName) { + kus.Transformers = append(kus.Transformers, transformerAnnotationFileName) + } + } if kg.kustomization.Spec.TargetNamespace != "" { @@ -277,13 +278,52 @@ func (kg *KustomizeGenerator) checksum(ctx context.Context, dirPath string) (str return fmt.Sprintf("%x", sha1.Sum(resources)), nil } +func (kg *KustomizeGenerator) generateAnnotationTransformer(checksum, dirPath string) error { + var annotations map[string]string + // add checksum annotations only if GC is enabled + if kg.kustomization.Spec.Prune { + annotations = gcAnnotation(checksum) + } + + var lt = struct { + ApiVersion string `json:"apiVersion" yaml:"apiVersion"` + Kind string `json:"kind" yaml:"kind"` + Metadata struct { + Name string `json:"name" yaml:"name"` + } `json:"metadata" yaml:"metadata"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + FieldSpecs []kustypes.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` + }{ + ApiVersion: "builtin", + Kind: "AnnotationsTransformer", + Metadata: struct { + Name string `json:"name" yaml:"name"` + }{ + Name: kg.kustomization.GetName(), + }, + Annotations: annotations, + FieldSpecs: []kustypes.FieldSpec{ + {Path: "metadata/annotations", CreateIfNotPresent: true}, + }, + } + + data, err := yaml.Marshal(lt) + if err != nil { + return err + } + + annotationsFile := filepath.Join(dirPath, transformerAnnotationFileName) + if err := ioutil.WriteFile(annotationsFile, data, os.ModePerm); err != nil { + return err + } + + return nil +} + func (kg *KustomizeGenerator) generateLabelTransformer(checksum, dirPath string) error { labels := selectorLabels(kg.kustomization.GetName(), kg.kustomization.GetNamespace()) - // add checksum label only if GC is enabled - if kg.kustomization.Spec.Prune { - labels = gcLabels(kg.kustomization.GetName(), kg.kustomization.GetNamespace(), checksum) - } + labels = gcLabels(kg.kustomization.GetName(), kg.kustomization.GetNamespace(), checksum) var lt = struct { ApiVersion string `json:"apiVersion" yaml:"apiVersion"` @@ -358,3 +398,13 @@ func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, e k := krusty.MakeKustomizer(buildOptions) return k.Run(fs, dirPath) } + +func find(source []string, value string) bool { + for _, item := range source { + if item == value { + return true + } + } + + return false +}