Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions api/v1beta1/artifactgenerator_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions docs/spec/v1beta1/artifactgenerators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions internal/controller/artifactgenerator_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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())

Expand All @@ -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{
Expand Down
107 changes: 107 additions & 0 deletions internal/controller/artifactgenerator_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading