From 45ebdbb19dd8a794c7ec45604b5c3b28c14d3be1 Mon Sep 17 00:00:00 2001 From: Kumar Piyush Date: Sun, 24 May 2026 00:00:59 +0530 Subject: [PATCH] Add .spec.commonMetadata to Artifact Generator Signed-off-by: Kumar Piyush Assisted-by: Antigravity/Gemini --- api/v1beta1/artifactgenerator_types.go | 17 +++ api/v1beta1/zz_generated.deepcopy.go | 34 ++++++ ...tensions.fluxcd.io_artifactgenerators.yaml | 17 +++ docs/spec/v1beta1/artifactgenerators.md | 21 ++++ .../artifactgenerator_controller.go | 18 ++- .../artifactgenerator_controller_test.go | 107 ++++++++++++++++++ 6 files changed, 211 insertions(+), 3 deletions(-) diff --git a/api/v1beta1/artifactgenerator_types.go b/api/v1beta1/artifactgenerator_types.go index eeb7f5b..93a5d77 100644 --- a/api/v1beta1/artifactgenerator_types.go +++ b/api/v1beta1/artifactgenerator_types.go @@ -42,8 +42,25 @@ const ( DisabledValue = "disabled" ) +// CommonMetadata defines the common labels and annotations. +type CommonMetadata struct { + // Annotations to be added to the object's metadata. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + + // Labels to be added to the object's metadata. + // +optional + Labels map[string]string `json:"labels,omitempty"` +} + // ArtifactGeneratorSpec defines the desired state of ArtifactGenerator. type ArtifactGeneratorSpec struct { + // CommonMetadata specifies the common labels and annotations that are + // applied to all resources. Any existing label or annotation will be + // overridden if its key matches a common one. + // +optional + CommonMetadata *CommonMetadata `json:"commonMetadata,omitempty"` + // Sources is a list of references to the Flux source-controller // resources that will be used to generate the artifact. // +kubebuilder:validation:MinItems=1 diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index a498461..cd29fb7 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -87,6 +87,11 @@ func (in *ArtifactGeneratorList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArtifactGeneratorSpec) DeepCopyInto(out *ArtifactGeneratorSpec) { *out = *in + if in.CommonMetadata != nil { + in, out := &in.CommonMetadata, &out.CommonMetadata + *out = new(CommonMetadata) + (*in).DeepCopyInto(*out) + } if in.Sources != nil { in, out := &in.Sources, &out.Sources *out = make([]SourceReference, len(*in)) @@ -139,6 +144,35 @@ func (in *ArtifactGeneratorStatus) DeepCopy() *ArtifactGeneratorStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonMetadata) DeepCopyInto(out *CommonMetadata) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonMetadata. +func (in *CommonMetadata) DeepCopy() *CommonMetadata { + if in == nil { + return nil + } + out := new(CommonMetadata) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CopyOperation) DeepCopyInto(out *CopyOperation) { *out = *in diff --git a/config/crd/bases/source.extensions.fluxcd.io_artifactgenerators.yaml b/config/crd/bases/source.extensions.fluxcd.io_artifactgenerators.yaml index c585512..54b7f28 100644 --- a/config/crd/bases/source.extensions.fluxcd.io_artifactgenerators.yaml +++ b/config/crd/bases/source.extensions.fluxcd.io_artifactgenerators.yaml @@ -139,6 +139,23 @@ spec: maxItems: 1000 minItems: 1 type: array + commonMetadata: + description: |- + CommonMetadata specifies the common labels and annotations that are + applied to all resources. Any existing label or annotation will be + overridden if its key matches a common one. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the object's metadata. + type: object + labels: + additionalProperties: + type: string + description: Labels to be added to the object's metadata. + type: object + type: object sources: description: |- Sources is a list of references to the Flux source-controller diff --git a/docs/spec/v1beta1/artifactgenerators.md b/docs/spec/v1beta1/artifactgenerators.md index 8fd6dd4..1c83475 100644 --- a/docs/spec/v1beta1/artifactgenerators.md +++ b/docs/spec/v1beta1/artifactgenerators.md @@ -356,6 +356,27 @@ Example of copy with `Extract` strategy: strategy, non-tarball files are silently skipped. For single file sources, the file must have a `.tar.gz` or `.tgz` extension. Directories are not supported with this strategy. +### Common Metadata + +The `.spec.commonMetadata` field defines labels and annotations that are uniformly applied to all +[ExternalArtifacts][externalartifact] generated by the ArtifactGenerator. This provides a mechanism +to propagate metadata to the output artifacts, which is particularly useful for enabling label +selectors in downstream components. + +```yaml +spec: + commonMetadata: + labels: + app.kubernetes.io/name: my-app + env: prod + annotations: + description: "Generated composite artifact" +``` + +Any existing label or annotation on the generated resources will be overridden if its key matches +a common one. Note that the `app.kubernetes.io/managed-by` and `source.extensions.fluxcd.io/generator` +labels are reserved by the controller and cannot be overridden by common metadata. + ## Working with ArtifactGenerators ### Suspend and Resume Reconciliation diff --git a/internal/controller/artifactgenerator_controller.go b/internal/controller/artifactgenerator_controller.go index ddd6e08..415a83c 100644 --- a/internal/controller/artifactgenerator_controller.go +++ b/internal/controller/artifactgenerator_controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "maps" "os" "path/filepath" "strings" @@ -427,6 +428,16 @@ func (r *ArtifactGeneratorReconciler) reconcileExternalArtifact(ctx context.Cont // Prepare labels for the ExternalArtifact with the managed-by and generator labels. labels := make(map[string]string) + var annotations map[string]string + + if cm := obj.Spec.CommonMetadata; cm != nil { + maps.Copy(labels, cm.Labels) + if len(cm.Annotations) > 0 { + annotations = make(map[string]string) + maps.Copy(annotations, cm.Annotations) + } + } + labels["app.kubernetes.io/managed-by"] = r.ControllerName labels[swapi.ArtifactGeneratorLabel] = string(obj.GetUID()) @@ -437,9 +448,10 @@ func (r *ArtifactGeneratorReconciler) reconcileExternalArtifact(ctx context.Cont Kind: sourcev1.ExternalArtifactKind, }, ObjectMeta: metav1.ObjectMeta{ - Name: outputArtifact.Name, - Namespace: obj.Namespace, - Labels: labels, + Name: outputArtifact.Name, + Namespace: obj.Namespace, + Labels: labels, + Annotations: annotations, }, Spec: sourcev1.ExternalArtifactSpec{ SourceRef: &gotkmeta.NamespacedObjectKindReference{ diff --git a/internal/controller/artifactgenerator_controller_test.go b/internal/controller/artifactgenerator_controller_test.go index f2c34bb..2829983 100644 --- a/internal/controller/artifactgenerator_controller_test.go +++ b/internal/controller/artifactgenerator_controller_test.go @@ -529,6 +529,113 @@ func TestArtifactGeneratorReconciler_fetchSources(t *testing.T) { } } +func TestArtifactGeneratorReconciler_CommonMetadata(t *testing.T) { + g := NewWithT(t) + reconciler := getArtifactGeneratorReconciler() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Create a namespace + ns, err := testEnv.CreateNamespace(ctx, "test-cm") + g.Expect(err).ToNot(HaveOccurred()) + + // Create the ArtifactGenerator object + objKey := client.ObjectKey{ + Name: "test-cm-gen", + Namespace: ns.Name, + } + obj := getArtifactGenerator(objKey) + obj.Spec.Sources = []swapi.SourceReference{ + { + Alias: fmt.Sprintf("%s-git", objKey.Name), + Kind: sourcev1.GitRepositoryKind, + Name: objKey.Name, + }, + } + obj.Spec.OutputArtifacts = []swapi.OutputArtifact{ + { + Name: fmt.Sprintf("%s-git", objKey.Name), + Copy: []swapi.CopyOperation{ + { + From: fmt.Sprintf("@%s-git/**", objKey.Name), + To: "@artifact/", + }, + }, + }, + } + obj.Spec.CommonMetadata = &swapi.CommonMetadata{ + Labels: map[string]string{ + "test-label": "true", + }, + Annotations: map[string]string{ + "test-annotation": "true", + }, + } + err = testClient.Create(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + // Create the GitRepository source + gitFiles := []gotktestsrv.File{ + {Name: "app.yaml", Body: "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test-app"}, + } + err = applyGitRepository(objKey, "main@sha256:abc123", gitFiles) + g.Expect(err).ToNot(HaveOccurred()) + + // Initialize the ArtifactGenerator with the finalizer + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: objKey, + }) + g.Expect(err).ToNot(HaveOccurred()) + + // Reconcile to process the sources and build artifacts + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: objKey, + }) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("sets labels and annotations", func(t *testing.T) { + g := NewWithT(t) + err = testClient.Get(ctx, objKey, obj) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gotkconditions.IsReady(obj)).To(BeTrue()) + + externalArtifact := &sourcev1.ExternalArtifact{} + key := client.ObjectKey{Name: fmt.Sprintf("%s-git", obj.Name), Namespace: obj.Namespace} + err = testClient.Get(ctx, key, externalArtifact) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(externalArtifact.Labels).To(HaveKeyWithValue("test-label", "true")) + g.Expect(externalArtifact.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", controllerName)) + g.Expect(externalArtifact.Labels).To(HaveKeyWithValue(swapi.ArtifactGeneratorLabel, string(obj.GetUID()))) + g.Expect(externalArtifact.Annotations).To(HaveKeyWithValue("test-annotation", "true")) + }) + + t.Run("removes labels and annotations", func(t *testing.T) { + g := NewWithT(t) + err = testClient.Get(ctx, objKey, obj) + g.Expect(err).ToNot(HaveOccurred()) + + obj.Spec.CommonMetadata = nil + err = testClient.Update(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + // Reconcile to process the update + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: objKey, + }) + g.Expect(err).ToNot(HaveOccurred()) + + externalArtifact := &sourcev1.ExternalArtifact{} + key := client.ObjectKey{Name: fmt.Sprintf("%s-git", obj.Name), Namespace: obj.Namespace} + err = testClient.Get(ctx, key, externalArtifact) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(externalArtifact.Labels).ToNot(HaveKey("test-label")) + g.Expect(externalArtifact.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", controllerName)) + g.Expect(externalArtifact.Annotations).ToNot(HaveKey("test-annotation")) + }) +} + func getArtifactGeneratorReconciler() *ArtifactGeneratorReconciler { return &ArtifactGeneratorReconciler{ ControllerName: controllerName,