diff --git a/api/v1/kustomization_types.go b/api/v1/kustomization_types.go index 59a86c8a..6bd217c3 100644 --- a/api/v1/kustomization_types.go +++ b/api/v1/kustomization_types.go @@ -177,6 +177,13 @@ type KustomizationSpec struct { // +optional Wait bool `json:"wait,omitempty"` + // BuildMetadata specifies which kustomize build metadata should be added + // to the built resources. The allowed values are 'originAnnotations' to + // annotate resources with their source origin, and 'transformerAnnotations' + // to annotate resources with the transformers that produced them. + // +optional + BuildMetadata []BuildMetadataOption `json:"buildMetadata,omitempty"` + // Components specifies relative paths to kustomize Components. // +optional Components []string `json:"components,omitempty"` @@ -194,6 +201,19 @@ type KustomizationSpec struct { HealthCheckExprs []kustomize.CustomHealthCheck `json:"healthCheckExprs,omitempty"` } +// BuildMetadataOption defines the supported buildMetadata options. +// +kubebuilder:validation:Enum=originAnnotations;transformerAnnotations +type BuildMetadataOption string + +const ( + // BuildMetadataOriginAnnotations enables config.kubernetes.io/origin annotations + // that track which file and path each resource was loaded from. + BuildMetadataOriginAnnotations BuildMetadataOption = "originAnnotations" + // BuildMetadataTransformerAnnotations enables internal.config.kubernetes.io annotations + // that record which kustomize transformers modified each resource. + BuildMetadataTransformerAnnotations BuildMetadataOption = "transformerAnnotations" +) + // CommonMetadata defines the common labels and annotations. type CommonMetadata struct { // Annotations to be added to the object's metadata. diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index e8122756..231b5b52 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -222,6 +222,11 @@ func (in *KustomizationSpec) DeepCopyInto(out *KustomizationSpec) { *out = new(metav1.Duration) **out = **in } + if in.BuildMetadata != nil { + in, out := &in.BuildMetadata, &out.BuildMetadata + *out = make([]BuildMetadataOption, len(*in)) + copy(*out, *in) + } if in.Components != nil { in, out := &in.Components, &out.Components *out = make([]string, len(*in)) diff --git a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml index ceda2cb4..ccda40e1 100644 --- a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml +++ b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml @@ -53,6 +53,20 @@ spec: KustomizationSpec defines the configuration to calculate the desired state from a Source using Kustomize. properties: + buildMetadata: + description: |- + BuildMetadata specifies which kustomize build metadata should be added + to the built resources. The allowed values are 'originAnnotations' to + annotate resources with their source origin, and 'transformerAnnotations' + to annotate resources with the transformers that produced them. + items: + description: BuildMetadataOption defines the supported buildMetadata + options. + enum: + - originAnnotations + - transformerAnnotations + type: string + type: array commonMetadata: description: |- CommonMetadata specifies the common labels and annotations that are diff --git a/docs/api/v1/kustomize.md b/docs/api/v1/kustomize.md index 582dc5f4..11624c6c 100644 --- a/docs/api/v1/kustomize.md +++ b/docs/api/v1/kustomize.md @@ -385,6 +385,23 @@ resources. When enabled, the HealthChecks are ignored. Defaults to false.

+buildMetadata
+ + +[]BuildMetadataOption + + + + +(Optional) +

BuildMetadata specifies which kustomize build metadata should be added +to the built resources. The allowed values are ‘originAnnotations’ to +annotate resources with their source origin, and ‘transformerAnnotations’ +to annotate resources with the transformers that produced them.

+ + + + components
[]string @@ -444,6 +461,13 @@ KustomizationStatus +

BuildMetadataOption +(string alias)

+

+(Appears on: +KustomizationSpec) +

+

BuildMetadataOption defines the supported buildMetadata options.

CommonMetadata

@@ -1018,6 +1042,23 @@ resources. When enabled, the HealthChecks are ignored. Defaults to false.

+buildMetadata
+ + +[]BuildMetadataOption + + + + +(Optional) +

BuildMetadata specifies which kustomize build metadata should be added +to the built resources. The allowed values are ‘originAnnotations’ to +annotate resources with their source origin, and ‘transformerAnnotations’ +to annotate resources with the transformers that produced them.

+ + + + components
[]string diff --git a/docs/spec/v1/kustomizations.md b/docs/spec/v1/kustomizations.md index 420f8000..ff9a7eaa 100644 --- a/docs/spec/v1/kustomizations.md +++ b/docs/spec/v1/kustomizations.md @@ -649,6 +649,41 @@ spec: digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3 ``` +### Build metadata + +`.spec.buildMetadata` is an optional list used to specify which +[Kustomize `buildMetadata`](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/buildmetadata/) +options should be added to the built resources. The allowed values are: + +- `originAnnotations`: Adds `config.kubernetes.io/origin` annotations that + track which file and path each resource was loaded from. +- `transformerAnnotations`: Adds `internal.config.kubernetes.io` annotations + that record which kustomize transformers modified each resource. + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: flux-system +spec: + # ...omitted for brevity + buildMetadata: + - originAnnotations +``` + +When `originAnnotations` is enabled, each resource gets an annotation like: + +```yaml +metadata: + annotations: + config.kubernetes.io/origin: | + path: apps/deployment.yaml +``` + +This is useful for debugging, auditing, and tooling that needs to trace +resources back to their source files. + ### Components `.spec.components` is an optional list used to specify diff --git a/go.mod b/go.mod index 04a5a4cb..ad1218f4 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/fluxcd/pkg/auth v0.40.0 github.com/fluxcd/pkg/cache v0.13.0 github.com/fluxcd/pkg/http/fetch v0.22.0 - github.com/fluxcd/pkg/kustomize v1.28.0 + github.com/fluxcd/pkg/kustomize v1.29.0 github.com/fluxcd/pkg/runtime v0.103.0 github.com/fluxcd/pkg/ssa v0.70.0 github.com/fluxcd/pkg/tar v0.17.0 diff --git a/go.sum b/go.sum index 8363d183..bfef6a9f 100644 --- a/go.sum +++ b/go.sum @@ -219,8 +219,8 @@ github.com/fluxcd/pkg/envsubst v1.5.0 h1:S07mo+MkGhptdHA4pRze5HPKlc8tHxKswNdcMZi github.com/fluxcd/pkg/envsubst v1.5.0/go.mod h1:c3a8DYI855sZUubHFYQbjfjop6Wu4/zg1cLyf7SnCes= github.com/fluxcd/pkg/http/fetch v0.22.0 h1:FT8CfstPE/e7+KRxNrx8ZJ1Uj5rkR5wXOtvQJurNQ0U= github.com/fluxcd/pkg/http/fetch v0.22.0/go.mod h1:X+8wF3peP79TyyDSgCJiavz+fAcYaf7CRXSeu7ccsPA= -github.com/fluxcd/pkg/kustomize v1.28.0 h1:0RuFVczJRabbt8frHZ/ql8aqte6BOOKk274O09l6/hE= -github.com/fluxcd/pkg/kustomize v1.28.0/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M= +github.com/fluxcd/pkg/kustomize v1.29.0 h1:B/5hr9wX6INwaQAZ6BGKVNvZm++A6qjgorUfoaBAwPw= +github.com/fluxcd/pkg/kustomize v1.29.0/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M= github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk= github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw= github.com/fluxcd/pkg/sourceignore v0.17.0 h1:Z72nruRMhC15zIEpWoDrAcJcJ1El6QDnP/aRDfE4WOA= diff --git a/internal/controller/kustomization_transformer_test.go b/internal/controller/kustomization_transformer_test.go index ac1f7adf..954885d1 100644 --- a/internal/controller/kustomization_transformer_test.go +++ b/internal/controller/kustomization_transformer_test.go @@ -604,3 +604,104 @@ func checkSecret(list *corev1.SecretList, name string) bool { return false } + +func TestKustomizationReconciler_BuildMetadata(t *testing.T) { + g := NewWithT(t) + id := "bm-" + randStringRunes(5) + revision := "v1.0.0" + resultK := &kustomizev1.Kustomization{} + + 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 string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s +data: + key: val +`, name), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("bm-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("bm-%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: &meta.KubeConfigReference{ + SecretRef: &meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + Prune: true, + BuildMetadata: []kustomizev1.BuildMetadataOption{ + kustomizev1.BuildMetadataOriginAnnotations, + }, + TargetNamespace: id, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + t.Run("sets origin annotations", func(t *testing.T) { + g := NewWithT(t) + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return isReconcileSuccess(resultK) + }, timeout, time.Second).Should(BeTrue()) + kstatusCheck.CheckErr(ctx, resultK) + + var cm corev1.ConfigMap + g.Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: id, Namespace: id}, &cm)).To(Succeed()) + g.Expect(cm.GetAnnotations()).To(HaveKey("config.kubernetes.io/origin")) + }) + + t.Run("removes origin annotations", func(t *testing.T) { + g := NewWithT(t) + resultK.Spec.BuildMetadata = nil + g.Expect(k8sClient.Update(context.Background(), resultK)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return isReconcileSuccess(resultK) + }, timeout, time.Second).Should(BeTrue()) + kstatusCheck.CheckErr(ctx, resultK) + + var cm corev1.ConfigMap + g.Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: id, Namespace: id}, &cm)).To(Succeed()) + g.Expect(cm.GetAnnotations()).ToNot(HaveKey("config.kubernetes.io/origin")) + }) +}