From dd0a04cc6430e348af15c1da13448499c1123b8c Mon Sep 17 00:00:00 2001 From: Ruslan Shaydullin Date: Tue, 4 Aug 2026 14:38:24 +0500 Subject: [PATCH] Expose OCI origin revision in events OCI artifacts can record their source revision in the org.opencontainers.image.revision annotation. Include that value in successful events so notification consumers can correlate an artifact with its source commit. Keep the existing artifact revision unchanged and omit the extra event metadata when the annotation is empty. Signed-off-by: Ruslan Shaydullin --- docs/spec/v1/ocirepositories.md | 7 ++++ .../controller/ocirepository_controller.go | 3 ++ .../ocirepository_controller_test.go | 36 +++++++++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/docs/spec/v1/ocirepositories.md b/docs/spec/v1/ocirepositories.md index fa3c5a2d7..455d69ae5 100644 --- a/docs/spec/v1/ocirepositories.md +++ b/docs/spec/v1/ocirepositories.md @@ -953,6 +953,13 @@ LAST SEEN TYPE REASON OBJECT 94s Warning OCIOperationFailed ocirepository/ failed to pull artifact from 'oci://ghcr.io/stefanprodan/manifests/podinfo': couldn't find tag "0.0.1" ``` +When `.status.artifact.metadata` contains a non-empty +`org.opencontainers.image.revision` annotation, the controller forwards its +value in `NewArtifact` and recovery `Succeeded` Events using the +`source.toolkit.fluxcd.io/originRevision` annotation. The +notification-controller exposes this value as `originRevision` in the Event +metadata sent to notification consumers. + Besides being reported in Events, the reconciliation errors are also logged by the controller. The Flux CLI offer commands for filtering the logs for a specific OCIRepository, e.g. diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go index ec941a1fd..34a053a1c 100644 --- a/internal/controller/ocirepository_controller.go +++ b/internal/controller/ocirepository_controller.go @@ -1336,6 +1336,9 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context, oldObj, newObj *so if val, ok := info[oci.RevisionAnnotation]; ok { revision = val } + if revision != "" { + annotations[fmt.Sprintf("%s/%s", sourcev1.GroupVersion.Group, eventv1.MetaOriginRevisionKey)] = revision + } if source != "" && revision != "" { message = fmt.Sprintf("%s, origin source '%s', origin revision '%s'", message, source, revision) } diff --git a/internal/controller/ocirepository_controller_test.go b/internal/controller/ocirepository_controller_test.go index 3b18e1fe1..fc4069e2c 100644 --- a/internal/controller/ocirepository_controller_test.go +++ b/internal/controller/ocirepository_controller_test.go @@ -60,6 +60,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/fluxcd/pkg/apis/meta" intdigest "github.com/fluxcd/pkg/artifact/digest" "github.com/fluxcd/pkg/artifact/storage" @@ -3413,13 +3414,14 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) { noopErr.Ignore = true tests := []struct { - name string - res sreconcile.Result - resErr error - oldObjBeforeFunc func(obj *sourcev1.OCIRepository) - newObjBeforeFunc func(obj *sourcev1.OCIRepository) - commit git.Commit - wantEvent string + name string + res sreconcile.Result + resErr error + oldObjBeforeFunc func(obj *sourcev1.OCIRepository) + newObjBeforeFunc func(obj *sourcev1.OCIRepository) + commit git.Commit + wantEvent string + wantOriginRevision string }{ { name: "error - no event", @@ -3441,7 +3443,8 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) { }, } }, - wantEvent: "Normal NewArtifact stored artifact with revision 'xxx' from 'oci://newurl.io', origin source 'https://github.com/stefanprodan/podinfo', origin revision '6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872'", + wantEvent: "Normal NewArtifact stored artifact with revision 'xxx' from 'oci://newurl.io', origin source 'https://github.com/stefanprodan/podinfo', origin revision '6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872'", + wantOriginRevision: "6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872", }, { name: "recovery from failure", @@ -3454,10 +3457,17 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) { }, newObjBeforeFunc: func(obj *sourcev1.OCIRepository) { obj.Spec.URL = "oci://newurl.io" - obj.Status.Artifact = &meta.Artifact{Revision: "xxx", Digest: "yyy"} + obj.Status.Artifact = &meta.Artifact{ + Revision: "xxx", + Digest: "yyy", + Metadata: map[string]string{ + oci.RevisionAnnotation: "6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872", + }, + } conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready") }, - wantEvent: "Normal Succeeded stored artifact with revision 'xxx' from 'oci://newurl.io'", + wantEvent: "Normal Succeeded stored artifact with revision 'xxx' from 'oci://newurl.io'", + wantOriginRevision: "6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872", }, { name: "recovery and new artifact", @@ -3525,6 +3535,12 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) { g.Expect(ok).To(Equal(tt.wantEvent != ""), "unexpected event received") if tt.wantEvent != "" { g.Expect(x).To(ContainSubstring(tt.wantEvent)) + originRevisionKey := fmt.Sprintf("%s/%s", sourcev1.GroupVersion.Group, eventv1.MetaOriginRevisionKey) + if tt.wantOriginRevision != "" { + g.Expect(x).To(ContainSubstring(fmt.Sprintf("%s:%s", originRevisionKey, tt.wantOriginRevision))) + } else { + g.Expect(x).NotTo(ContainSubstring(originRevisionKey)) + } } default: if tt.wantEvent != "" {