Skip to content

Commit c1a5759

Browse files
committed
Implement origin revision tracking
Signed-off-by: Stefan Prodan <[email protected]>
1 parent 6837b7a commit c1a5759

File tree

6 files changed

+119
-40
lines changed

6 files changed

+119
-40
lines changed

api/v1beta1/artifactgenerator_types.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ import (
2626
)
2727

2828
const (
29-
ArtifactGeneratorKind = "ArtifactGenerator"
30-
Finalizer = "source.extensions.fluxcd.io/finalizer"
31-
ArtifactGeneratorLabel = "source.extensions.fluxcd.io/generator"
32-
ReconcileAnnotation = "source.extensions.fluxcd.io/reconcile"
33-
ReconciliationDisabledReason = "ReconciliationDisabled"
34-
AccessDeniedReason = "AccessDenied"
35-
ValidationFailedReason = "ValidationFailed"
36-
SourceFetchFailedReason = "SourceFetchFailed"
37-
EnabledValue = "enabled"
38-
DisabledValue = "disabled"
29+
ArtifactGeneratorKind = "ArtifactGenerator"
30+
Finalizer = "source.extensions.fluxcd.io/finalizer"
31+
ArtifactGeneratorLabel = "source.extensions.fluxcd.io/generator"
32+
ArtifactOriginRevisionAnnotation = "org.opencontainers.image.revision"
33+
ReconcileAnnotation = "source.extensions.fluxcd.io/reconcile"
34+
ReconciliationDisabledReason = "ReconciliationDisabled"
35+
AccessDeniedReason = "AccessDenied"
36+
ValidationFailedReason = "ValidationFailed"
37+
SourceFetchFailedReason = "SourceFetchFailed"
38+
EnabledValue = "enabled"
39+
DisabledValue = "disabled"
3940
)
4041

4142
// ArtifactGeneratorSpec defines the desired state of ArtifactGenerator.
@@ -102,6 +103,17 @@ type OutputArtifact struct {
102103
// +optional
103104
Revision string `json:"revision,omitempty"`
104105

106+
// OriginRevision is used to set the 'org.opencontainers.image.revision'
107+
// annotation on the generated artifact metadata.
108+
// If specified, it must point to an existing source alias in the format "@<alias>".
109+
// If the referenced source has an origin revision (e.g. a Git commit SHA),
110+
// it will be used to set the annotation on the generated artifact.
111+
// If the referenced source does not have an origin revision, the field is ignored.
112+
// +kubebuilder:validation:Pattern="^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)$"
113+
// +kubebuilder:validation:MaxLength=64
114+
// +optional
115+
OriginRevision string `json:"originRevision,omitempty"`
116+
105117
// Copy defines a list of copy operations to perform from the sources to the generated artifact.
106118
// The copy operations are performed in the order they are listed with existing files
107119
// being overwritten by later copy operations.

api/v1beta1/observed_source.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,21 @@ import (
2828
// an artifact in the ArtifactGeneratorStatus.ObservedSourcesDigest field.
2929
type ObservedSource struct {
3030
// Digest is the artifact digest of the upstream source.
31+
// +required
3132
Digest string `json:"digest"`
3233

3334
// Revision is the artifact revision of the upstream source.
35+
// +required
3436
Revision string `json:"revision"`
3537

38+
// OriginRevision holds the origin revision of the upstream source,
39+
// extracted from the 'org.opencontainers.image.revision' annotation,
40+
// if available in the source artifact metadata.
41+
// +optional
42+
OriginRevision string `json:"originRevision,omitempty"`
43+
3644
// URL is the artifact URL of the upstream source.
45+
// +required
3746
URL string `json:"url"`
3847
}
3948

config/crd/bases/source.extensions.fluxcd.io_artifactgenerators.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ spec:
7979
maxLength: 253
8080
pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$
8181
type: string
82+
originRevision:
83+
description: |-
84+
OriginRevision is used to set the 'org.opencontainers.image.revision'
85+
annotation on the generated artifact metadata.
86+
If specified, it must point to an existing source alias in the format "@<alias>".
87+
If the referenced source has an origin revision (e.g. a Git commit SHA),
88+
it will be used to set the annotation on the generated artifact.
89+
If the referenced source does not have an origin revision, the field is ignored.
90+
maxLength: 64
91+
pattern: ^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)$
92+
type: string
8293
revision:
8394
description: |-
8495
Revision is the revision of the generated artifact.

docs/spec/v1beta1/artifactgenerators.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ spec:
9090
namespace: apps
9191
artifacts:
9292
- name: podinfo-composite
93-
revision: "@chart"
93+
originRevision: "@chart"
9494
copy:
9595
- from: "@chart/"
9696
to: "@artifact/"
@@ -229,26 +229,20 @@ regenerate the affected artifacts automatically.
229229
The `.spec.artifacts` field defines the list of ExternalArtifacts to be generated from the sources.
230230
Each artifact must specify:
231231

232-
- `name`: The name of the generated ExternalArtifact resource. It must be unique in the context
232+
- `name` (required): The name of the generated ExternalArtifact resource. It must be unique in the context
233233
of the ArtifactGenerator and must conform to Kubernetes resource naming conventions.
234+
- `copy` (required): A list of copy operations to perform from sources to the artifact.
234235
- `revision` (optional): A specific source revision to use in the format `@alias`.
235236
If not specified, the revision is automatically computed as `latest@<digest>` based on the artifact content.
236-
- `copy`: A list of copy operations to perform from sources to the artifact.
237-
238-
#### Copy Operations
239-
240-
Each copy operation specifies how to copy files from sources into the generated artifact:
241-
242-
- `from`: Source path in the format `@alias/pattern` where `alias` references
243-
a source and `pattern` is a glob pattern or a specific file/directory path within that source.
244-
- `to`: Destination path in the format `@artifact/path` where `artifact` is
245-
the root of the generated artifact and `path` is the relative path to a file or directory.
237+
- `originRevision` (optional): A specific source origin revision to include in the artifact metadata
238+
in the format `@alias`. This is useful for sources of type `OCIRepository` to track the origin Git commit.
246239

247240
```yaml
248241
spec:
249242
artifacts:
250243
- name: my-app
251-
revision: "@backend" # Use backend source revision
244+
revision: "@backend"
245+
originRevision: "@frontend"
252246
copy:
253247
- from: "@backend/deploy/*.yaml"
254248
to: "@artifact/backend/"
@@ -258,6 +252,15 @@ spec:
258252
to: "@artifact/env.yaml"
259253
```
260254

255+
#### Copy Operations
256+
257+
Each copy operation specifies how to copy files from sources into the generated artifact:
258+
259+
- `from`: Source path in the format `@alias/pattern` where `alias` references
260+
a source and `pattern` is a glob pattern or a specific file/directory path within that source.
261+
- `to`: Destination path in the format `@artifact/path` where `artifact` is
262+
the root of the generated artifact and `path` is the relative path to a file or directory.
263+
261264
Copy operations use `cp`-like semantics:
262265

263266
- Operations are executed in order; later operations can overwrite files from earlier ones

internal/controller/artifactgenerator_controller.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,8 @@ func (r *ArtifactGeneratorReconciler) reconcile(ctx context.Context,
206206
return ctrl.Result{}, err
207207
}
208208

209-
// Override the revision with the one from the source if specified.
210-
if oa.Revision != "" {
211-
if rs, ok := remoteSources[strings.TrimPrefix(oa.Revision, "@")]; ok {
212-
artifact.Revision = rs.Revision
213-
}
214-
}
209+
// Set the revision and origin revision metadata on the artifact.
210+
r.setArtifactRevisions(artifact, oa, remoteSources)
215211

216212
// Reconcile the ExternalArtifact corresponding to the built artifact.
217213
// The ExternalArtifact will reference the artifact stored in the storage backend.
@@ -314,15 +310,23 @@ func (r *ArtifactGeneratorReconciler) observeSources(ctx context.Context,
314310
src.Name, src.Kind)
315311
}
316312

317-
if source.GetArtifact() == nil {
313+
artifact := source.GetArtifact()
314+
if artifact == nil {
318315
return nil, fmt.Errorf("source '%s/%s' is not ready", src.Kind, namespacedName)
319316
}
320317

321-
observedSources[src.Alias] = swapi.ObservedSource{
322-
Digest: source.GetArtifact().Digest,
323-
Revision: source.GetArtifact().Revision,
324-
URL: source.GetArtifact().URL,
318+
observedSource := swapi.ObservedSource{
319+
Digest: artifact.Digest,
320+
Revision: artifact.Revision,
321+
URL: artifact.URL,
322+
}
323+
324+
// Capture the origin revision if present in the artifact metadata.
325+
if originRev, ok := artifact.Metadata[swapi.ArtifactOriginRevisionAnnotation]; ok {
326+
observedSource.OriginRevision = originRev
325327
}
328+
329+
observedSources[src.Alias] = observedSource
326330
}
327331

328332
return observedSources, nil
@@ -428,12 +432,15 @@ func (r *ArtifactGeneratorReconciler) reconcileExternalArtifact(ctx context.Cont
428432
return nil, fmt.Errorf("failed to patch ExternalArtifact status: %w", err)
429433
}
430434

435+
// Log if the artifact is up to date or emit an event if it is new or has changed.
431436
if obj.HasArtifactInInventory(ea.Name, ea.Namespace, artifact.Digest) {
432437
log.Info(fmt.Sprintf("%s/%s/%s is up to date",
433438
ea.Kind, ea.Namespace, ea.Name))
434439
} else {
435-
log.Info(fmt.Sprintf("%s/%s/%s reconciled with revision %s",
436-
ea.Kind, ea.Namespace, ea.Name, artifact.Revision))
440+
msg := fmt.Sprintf("%s/%s/%s reconciled with revision %s",
441+
ea.Kind, ea.Namespace, ea.Name, artifact.Revision)
442+
log.Info(msg)
443+
r.Event(obj, corev1.EventTypeNormal, meta.ReadyCondition, msg)
437444
}
438445

439446
return &swapi.ExternalArtifactReference{
@@ -467,3 +474,30 @@ func (r *ArtifactGeneratorReconciler) findOrphanedReferences(
467474

468475
return orphaned
469476
}
477+
478+
// setArtifactRevisions sets the revision and origin revision metadata on the artifact
479+
// based on the output artifact configuration and available remote sources.
480+
func (r *ArtifactGeneratorReconciler) setArtifactRevisions(artifact *meta.Artifact,
481+
oa swapi.OutputArtifact,
482+
remoteSources map[string]swapi.ObservedSource) {
483+
// Override the revision with the one from the source if specified.
484+
if oa.Revision != "" {
485+
if rs, ok := remoteSources[strings.TrimPrefix(oa.Revision, "@")]; ok {
486+
artifact.Revision = rs.Revision
487+
}
488+
}
489+
490+
// Set the origin revision in the artifact metadata if available from the source.
491+
if oa.OriginRevision != "" {
492+
if artifact.Metadata == nil {
493+
artifact.Metadata = make(map[string]string)
494+
}
495+
if rs, ok := remoteSources[strings.TrimPrefix(oa.OriginRevision, "@")]; ok {
496+
if rs.OriginRevision != "" {
497+
artifact.Metadata[swapi.ArtifactOriginRevisionAnnotation] = rs.OriginRevision
498+
} else {
499+
artifact.Metadata[swapi.ArtifactOriginRevisionAnnotation] = rs.Revision
500+
}
501+
}
502+
}
503+
}

internal/controller/artifactgenerator_controller_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ func TestArtifactGeneratorReconciler_Reconcile(t *testing.T) {
143143

144144
if inv.Name == fmt.Sprintf("%s-oci", obj.Name) {
145145
ociArtifactDigest = externalArtifact.Status.Artifact.Digest
146+
147+
// Verify that the ExternalArtifact inherited the origin revision of the OCIRepository
148+
originRev, ok := externalArtifact.Status.Artifact.Metadata[swapi.ArtifactOriginRevisionAnnotation]
149+
g.Expect(ok).To(BeTrue(), "expected origin revision in metadata")
150+
g.Expect(originRev).To(Equal("main@sha1:xyz123"))
146151
}
147152
}
148153

@@ -245,9 +250,10 @@ func TestArtifactGeneratorReconciler_Reconcile(t *testing.T) {
245250
// Verify events were recorded
246251
events := getEvents(obj.Name, obj.Namespace)
247252
g.Expect(events).ToNot(BeEmpty())
248-
g.Expect(events[0].Type).To(Equal(corev1.EventTypeNormal))
249-
g.Expect(events[0].Reason).To(Equal(meta.ReadyCondition))
250-
g.Expect(events[0].Message).To(ContainSubstring("reconciliation succeeded"))
253+
for _, e := range events {
254+
g.Expect(e.Type).To(Equal(corev1.EventTypeNormal))
255+
g.Expect(e.Reason).To(Equal(meta.ReadyCondition))
256+
}
251257

252258
// Delete the object to trigger finalization
253259
err = testClient.Delete(ctx, obj)
@@ -571,7 +577,8 @@ func getArtifactGenerator(objectKey client.ObjectKey) *swapi.ArtifactGenerator {
571577
},
572578
},
573579
{
574-
Name: fmt.Sprintf("%s-oci", objectKey.Name),
580+
Name: fmt.Sprintf("%s-oci", objectKey.Name),
581+
OriginRevision: fmt.Sprintf("@%s-oci", objectKey.Name),
575582
Copy: []swapi.CopyOperation{
576583
{
577584
From: fmt.Sprintf("@%s-oci/**", objectKey.Name),
@@ -689,6 +696,9 @@ func applyOCIRepository(objKey client.ObjectKey, revision string, files []testse
689696
Revision: revision,
690697
Digest: dig.String(),
691698
LastUpdateTime: metav1.Now(),
699+
Metadata: map[string]string{
700+
swapi.ArtifactOriginRevisionAnnotation: "main@sha1:xyz123",
701+
},
692702
},
693703
}
694704

0 commit comments

Comments
 (0)