diff --git a/controllers/kustomization_controller.go b/controllers/kustomization_controller.go index 579f61f1..46db6a54 100644 --- a/controllers/kustomization_controller.go +++ b/controllers/kustomization_controller.go @@ -649,6 +649,9 @@ func (r *KustomizationReconciler) apply(ctx context.Context, manager *ssa.Resour applyOpts := ssa.DefaultApplyOptions() applyOpts.Force = kustomization.Spec.Force + applyOpts.Exclusions = map[string]string{ + fmt.Sprintf("%s/reconcile", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + } // contains only CRDs and Namespaces var stageOne []*unstructured.Unstructured @@ -796,7 +799,8 @@ func (r *KustomizationReconciler) prune(ctx context.Context, manager *ssa.Resour PropagationPolicy: metav1.DeletePropagationBackground, Inclusions: manager.GetOwnerLabels(kustomization.Name, kustomization.Namespace), Exclusions: map[string]string{ - fmt.Sprintf("%s/prune", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + fmt.Sprintf("%s/prune", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + fmt.Sprintf("%s/reconcile", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, }, } @@ -840,7 +844,8 @@ func (r *KustomizationReconciler) finalize(ctx context.Context, kustomization ku PropagationPolicy: metav1.DeletePropagationBackground, Inclusions: resourceManager.GetOwnerLabels(kustomization.Name, kustomization.Namespace), Exclusions: map[string]string{ - fmt.Sprintf("%s/prune", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + fmt.Sprintf("%s/prune", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + fmt.Sprintf("%s/reconcile", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, }, } diff --git a/controllers/kustomization_force_test.go b/controllers/kustomization_force_test.go index 10577602..53ec861e 100644 --- a/controllers/kustomization_force_test.go +++ b/controllers/kustomization_force_test.go @@ -19,16 +19,16 @@ package controllers import ( "context" "fmt" - "github.com/fluxcd/pkg/testserver" - corev1 "k8s.io/api/core/v1" - apimeta "k8s.io/apimachinery/pkg/api/meta" "testing" "time" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/testserver" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/controllers/kustomization_inventory_test.go b/controllers/kustomization_inventory_test.go new file mode 100644 index 00000000..d8dc546b --- /dev/null +++ b/controllers/kustomization_inventory_test.go @@ -0,0 +1,274 @@ +/* +Copyright 2021 The Flux authors + +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" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/cli-utils/pkg/object" + "testing" + "time" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/testserver" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + . "github.com/onsi/gomega" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" +) + +func TestKustomizationReconciler_Inventory(t *testing.T) { + g := NewWithT(t) + id := "inv-" + randStringRunes(5) + revision := "v1.0.0" + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name, data string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: "%[1]s" +data: + key: "%[2]s" +--- +apiVersion: v1 +kind: Secret +metadata: + name: "%[1]s" +stringData: + key: "%[2]s" +`, name, data), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id, id)) + g.Expect(err).NotTo(HaveOccurred()) + + url := fmt.Sprintf("%s/%s", testServer.URL(), artifact) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("inv-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, url, revision, "") + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("inv-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: 2 * time.Minute}, + Path: "./", + KubeConfig: &kustomizev1.KubeConfig{ + SecretRef: meta.LocalObjectReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + TargetNamespace: id, + Prune: true, + Timeout: &metav1.Duration{Duration: time.Second}, + Wait: true, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + resultK := &kustomizev1.Kustomization{} + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + ready := apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.ReadyCondition) + return ready && resultK.Status.LastAppliedRevision == revision + }, timeout, time.Second).Should(BeTrue()) + + configMap := &corev1.ConfigMap{} + configMapName := types.NamespacedName{Name: id, Namespace: id} + + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + } + secretName := types.NamespacedName{Name: id, Namespace: id} + + t.Run("creates resources", func(t *testing.T) { + g.Expect(k8sClient.Get(context.Background(), secretName, secret)).To(Succeed()) + g.Expect(secret.Data["key"]).To(Equal([]byte(id))) + + g.Expect(k8sClient.Get(context.Background(), configMapName, configMap)).To(Succeed()) + g.Expect(configMap.Data["key"]).To(Equal(id)) + + g.Expect(resultK.Status.Inventory.Entries).Should(ConsistOf([]kustomizev1.ResourceRef{ + { + ID: object.ObjMetadata{ + Namespace: id, + Name: id, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Secret", + }, + }.String(), + Version: "v1", + }, + { + ID: object.ObjMetadata{ + Namespace: id, + Name: id, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "ConfigMap", + }, + }.String(), + Version: "v1", + }, + })) + }) + + t.Run("ignores drift", func(t *testing.T) { + testRev := revision + "-1" + testVal := "test" + + g.Expect(k8sClient.Get(context.Background(), configMapName, configMap)).To(Succeed()) + configMapClone := configMap.DeepCopy() + configMapClone.Data["key"] = testVal + configMapClone.SetAnnotations(map[string]string{ + fmt.Sprintf("%s/reconcile", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + }) + g.Expect(k8sClient.Update(context.Background(), configMapClone)).To(Succeed()) + + err = applyGitRepository(repositoryName, url, testRev, "") + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + ready := apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.ReadyCondition) + return ready && resultK.Status.LastAppliedRevision == testRev + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(k8sClient.Get(context.Background(), configMapName, configMap)).To(Succeed()) + g.Expect(configMap.Data["key"]).To(Equal(testVal)) + }) + + t.Run("corrects drift", func(t *testing.T) { + testRev := revision + "-2" + + g.Expect(k8sClient.Get(context.Background(), configMapName, configMap)).To(Succeed()) + configMapClone := configMap.DeepCopy() + configMapClone.SetAnnotations(map[string]string{ + fmt.Sprintf("%s/reconcile", kustomizev1.GroupVersion.Group): "enabled", + }) + g.Expect(k8sClient.Update(context.Background(), configMapClone)).To(Succeed()) + + err = applyGitRepository(repositoryName, url, testRev, "") + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + ready := apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.ReadyCondition) + return ready && resultK.Status.LastAppliedRevision == testRev + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(k8sClient.Get(context.Background(), configMapName, configMap)).To(Succeed()) + g.Expect(configMap.Data["key"]).To(Equal(id)) + }) + + t.Run("renames resources", func(t *testing.T) { + testId := id + randStringRunes(5) + testRev := revision + "-3" + + g.Expect(k8sClient.Get(context.Background(), configMapName, configMap)).To(Succeed()) + configMapClone := configMap.DeepCopy() + configMapClone.SetAnnotations(map[string]string{ + fmt.Sprintf("%s/reconcile", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + }) + g.Expect(k8sClient.Update(context.Background(), configMapClone)).To(Succeed()) + + artifact, err := testServer.ArtifactFromFiles(manifests(testId, id)) + g.Expect(err).NotTo(HaveOccurred()) + url := fmt.Sprintf("%s/%s", testServer.URL(), artifact) + err = applyGitRepository(repositoryName, url, testRev, "") + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + ready := apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.ReadyCondition) + return ready && resultK.Status.LastAppliedRevision == testRev + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(resultK.Status.Inventory.Entries).Should(ConsistOf([]kustomizev1.ResourceRef{ + { + ID: object.ObjMetadata{ + Namespace: id, + Name: testId, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Secret", + }, + }.String(), + Version: "v1", + }, + { + ID: object.ObjMetadata{ + Namespace: id, + Name: testId, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "ConfigMap", + }, + }.String(), + Version: "v1", + }, + })) + + old := &corev1.Secret{} + err = k8sClient.Get(context.Background(), secretName, old) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + g.Expect(k8sClient.Get(context.Background(), configMapName, configMap)).To(Succeed()) + g.Expect(configMap.Data["key"]).To(Equal(id)) + }) +} diff --git a/controllers/kustomization_transformer_test.go b/controllers/kustomization_transformer_test.go index d0b8dfb3..1eb8f3cb 100644 --- a/controllers/kustomization_transformer_test.go +++ b/controllers/kustomization_transformer_test.go @@ -19,18 +19,18 @@ package controllers import ( "context" "fmt" - "github.com/fluxcd/pkg/apis/kustomize" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "strings" "testing" "time" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/fluxcd/pkg/apis/kustomize" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/docs/spec/v1beta2/kustomization.md b/docs/spec/v1beta2/kustomization.md index 3d2608c7..ce96c125 100644 --- a/docs/spec/v1beta2/kustomization.md +++ b/docs/spec/v1beta2/kustomization.md @@ -322,7 +322,7 @@ const ( On-demand execution example: -```bash +```sh kubectl annotate --overwrite kustomization/podinfo reconcile.fluxcd.io/requestedAt="$(date +%s)" ``` @@ -334,6 +334,16 @@ kubectl get all --all-namespaces \ -l=kustomize.toolkit.fluxcd.io/namespace="" ``` +You can configure the controller to ignore in-cluster resources by labeling or annotating them: + +```sh +kubectl annotate service/podinfo kustomize.toolkit.fluxcd.io/reconcile=disabled +``` + +Note that when the `kustomize.toolkit.fluxcd.io/reconcile` annotation is set to `disabled`, +the controller will no longer apply changes from source, nor will it prune the resource. +To resume reconciliation, set the annotation to `enabled` or remove it. + ## Garbage collection To enable garbage collection, set `spec.prune` to `true`.