From 62eb502ea1d7223ecabeba88e3572b15b35bef62 Mon Sep 17 00:00:00 2001 From: Sunny Date: Fri, 30 Sep 2022 17:52:26 +0530 Subject: [PATCH] OCIRepo: Add observed source config in status Replace content config checksum with explicit source config observations. It makes the observations of the controller more transparent and easier to debug. Introduces `observedIgnore` and `observedLayerSelector` status fields. Signed-off-by: Sunny --- api/v1beta2/ocirepository_types.go | 10 + api/v1beta2/zz_generated.deepcopy.go | 10 + ...rce.toolkit.fluxcd.io_ocirepositories.yaml | 23 ++ controllers/ocirepository_controller.go | 68 +++--- controllers/ocirepository_controller_test.go | 202 +++++++++++++++--- docs/api/source.md | 31 ++- docs/spec/v1beta2/ocirepositories.md | 47 ++++ 7 files changed, 325 insertions(+), 66 deletions(-) diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go index 91ca7f859..90742b2a2 100644 --- a/api/v1beta2/ocirepository_types.go +++ b/api/v1beta2/ocirepository_types.go @@ -214,6 +214,16 @@ type OCIRepositoryStatus struct { // +optional ContentConfigChecksum string `json:"contentConfigChecksum,omitempty"` + // ObservedIgnore is the observed exclusion patterns used for constructing + // the source artifact. + // +optional + ObservedIgnore *string `json:"observedIgnore,omitempty"` + + // ObservedLayerSelector is the observed layer selector used for constructing + // the source artifact. + // +optional + ObservedLayerSelector *OCILayerSelector `json:"observedLayerSelector,omitempty"` + meta.ReconcileRequestStatus `json:",inline"` } diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index b759c3791..f75ab3151 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -777,6 +777,16 @@ func (in *OCIRepositoryStatus) DeepCopyInto(out *OCIRepositoryStatus) { *out = new(Artifact) (*in).DeepCopyInto(*out) } + if in.ObservedIgnore != nil { + in, out := &in.ObservedIgnore, &out.ObservedIgnore + *out = new(string) + **out = **in + } + if in.ObservedLayerSelector != nil { + in, out := &in.ObservedLayerSelector, &out.ObservedLayerSelector + *out = new(OCILayerSelector) + **out = **in + } out.ReconcileRequestStatus = in.ReconcileRequestStatus } diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml index 2d236ec99..36ee404e1 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml @@ -317,6 +317,29 @@ spec: description: ObservedGeneration is the last observed generation. format: int64 type: integer + observedIgnore: + description: ObservedIgnore is the observed exclusion patterns used + for constructing the source artifact. + type: string + observedLayerSelector: + description: ObservedLayerSelector is the observed layer selector + used for constructing the source artifact. + properties: + mediaType: + description: MediaType specifies the OCI media type of the layer + which should be extracted from the OCI Artifact. The first layer + matching this type is selected. + type: string + operation: + description: Operation specifies how the selected layer should + be processed. By default, the layer compressed content is extracted + to storage. When the operation is set to 'copy', the layer compressed + content is persisted to storage as it is. + enum: + - extract + - copy + type: string + type: object url: description: URL is the download link for the artifact output of the last OCI Repository sync. diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index 2a6d44429..c41d630f3 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -18,7 +18,6 @@ package controllers import ( "context" - "crypto/sha256" "crypto/tls" "crypto/x509" "errors" @@ -44,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" kuberecorder "k8s.io/client-go/tools/record" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -427,10 +427,9 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision %s", revision) } - // Skip pulling if the artifact revision and the content config checksum has + // Skip pulling if the artifact revision and the source configuration has // not changed. - if obj.GetArtifact().HasRevision(revision) && - r.calculateContentConfigChecksum(obj) == obj.Status.ContentConfigChecksum { + if obj.GetArtifact().HasRevision(revision) && !ociSourceConfigChanged(obj) { conditions.Delete(obj, sourcev1.FetchFailedCondition) return sreconcile.ResultSuccess, nil } @@ -918,13 +917,9 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *so artifact := r.Storage.NewArtifactFor(obj.Kind, obj, revision, fmt.Sprintf("%s.tar.gz", r.digestFromRevision(revision))) - // Calculate the content config checksum. - ccc := r.calculateContentConfigChecksum(obj) - // Set the ArtifactInStorageCondition if there's no drift. defer func() { - if obj.GetArtifact().HasRevision(artifact.Revision) && - obj.Status.ContentConfigChecksum == ccc { + if obj.GetArtifact().HasRevision(artifact.Revision) && !ociSourceConfigChanged(obj) { conditions.Delete(obj, sourcev1.ArtifactOutdatedCondition) conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for digest '%s'", artifact.Revision) @@ -932,8 +927,7 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *so }() // The artifact is up-to-date - if obj.GetArtifact().HasRevision(artifact.Revision) && - obj.Status.ContentConfigChecksum == ccc { + if obj.GetArtifact().HasRevision(artifact.Revision) && !ociSourceConfigChanged(obj) { r.eventLogf(ctx, obj, events.EventTypeTrace, sourcev1.ArtifactUpToDateReason, "artifact up-to-date with remote revision: '%s'", artifact.Revision) return sreconcile.ResultSuccess, nil @@ -1008,10 +1002,12 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *so } } - // Record it on the object + // Record the observations on the object. obj.Status.Artifact = artifact.DeepCopy() obj.Status.Artifact.Metadata = metadata.Metadata - obj.Status.ContentConfigChecksum = ccc + obj.Status.ContentConfigChecksum = "" // To be removed in the next API version. + obj.Status.ObservedIgnore = obj.Spec.Ignore + obj.Status.ObservedLayerSelector = obj.Spec.LayerSelector // Update symlink on a "best effort" basis url, err := r.Storage.Symlink(artifact, "latest.tar.gz") @@ -1141,24 +1137,6 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context, oldObj, newObj *so } } -// calculateContentConfigChecksum calculates a checksum of all the -// configurations that result in a change in the source artifact. It can be used -// to decide if further reconciliation is needed when an artifact already exists -// for a set of configurations. -func (r *OCIRepositoryReconciler) calculateContentConfigChecksum(obj *sourcev1.OCIRepository) string { - c := []byte{} - // Consider the ignore rules. - if obj.Spec.Ignore != nil { - c = append(c, []byte(*obj.Spec.Ignore)...) - } - // Consider the layer selector. - if obj.Spec.LayerSelector != nil { - c = append(c, []byte(obj.GetLayerMediaType()+obj.GetLayerOperation())...) - } - - return fmt.Sprintf("sha256:%x", sha256.Sum256(c)) -} - // craneOptions sets the auth headers, timeout and user agent // for all operations against remote container registries. func craneOptions(ctx context.Context, insecure bool) []crane.Option { @@ -1208,3 +1186,31 @@ type remoteOptions struct { craneOpts []crane.Option verifyOpts []remote.Option } + +// ociSourceConfigChanged evaluates the current spec with the observations +// of the artifact in the status to determine if source configuration has +// changed and requires rebuilding the artifact. +func ociSourceConfigChanged(obj *sourcev1.OCIRepository) bool { + if !pointer.StringEqual(obj.Spec.Ignore, obj.Status.ObservedIgnore) { + return true + } + + if !layerSelectorEqual(obj.Spec.LayerSelector, obj.Status.ObservedLayerSelector) { + return true + } + + return false +} + +// Returns true if both arguments are nil or both arguments +// dereference to the same value. +// Based on k8s.io/utils/pointer/pointer.go pointer value equality. +func layerSelectorEqual(a, b *sourcev1.OCILayerSelector) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + return *a == *b +} diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index bdd861120..ee4b065bc 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -69,8 +69,6 @@ import ( "github.com/fluxcd/source-controller/pkg/git" ) -const ociRepoEmptyContentConfigChecksum = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - func TestOCIRepository_Reconcile(t *testing.T) { g := NewWithT(t) @@ -1290,21 +1288,48 @@ func TestOCIRepository_reconcileSource_noop(t *testing.T) { }, }, { - name: "noop - artifact revisions and ccc match", + name: "noop - artifact revisions match", beforeFunc: func(obj *sourcev1.OCIRepository) { obj.Status.Artifact = &sourcev1.Artifact{ Revision: testRevision, } - obj.Status.ContentConfigChecksum = ociRepoEmptyContentConfigChecksum }, afterFunc: func(g *WithT, artifact *sourcev1.Artifact) { g.Expect(artifact.Metadata).To(BeEmpty()) }, }, { - name: "full reconcile - same rev, different ccc", + name: "full reconcile - same rev, unobserved ignore", beforeFunc: func(obj *sourcev1.OCIRepository) { - obj.Status.ContentConfigChecksum = "some-checksum" + obj.Status.ObservedIgnore = pointer.String("aaa") + obj.Status.Artifact = &sourcev1.Artifact{ + Revision: testRevision, + } + }, + afterFunc: func(g *WithT, artifact *sourcev1.Artifact) { + g.Expect(artifact.Metadata).ToNot(BeEmpty()) + }, + }, + { + name: "noop - same rev, observed ignore", + beforeFunc: func(obj *sourcev1.OCIRepository) { + obj.Spec.Ignore = pointer.String("aaa") + obj.Status.ObservedIgnore = pointer.String("aaa") + obj.Status.Artifact = &sourcev1.Artifact{ + Revision: testRevision, + } + }, + afterFunc: func(g *WithT, artifact *sourcev1.Artifact) { + g.Expect(artifact.Metadata).To(BeEmpty()) + }, + }, + { + name: "full reconcile - same rev, unobserved layer selector", + beforeFunc: func(obj *sourcev1.OCIRepository) { + obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Operation: sourcev1.OCILayerCopy, + } obj.Status.Artifact = &sourcev1.Artifact{ Revision: testRevision, } @@ -1320,10 +1345,13 @@ func TestOCIRepository_reconcileSource_noop(t *testing.T) { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Operation: sourcev1.OCILayerCopy, } + obj.Status.ObservedLayerSelector = &sourcev1.OCILayerSelector{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Operation: sourcev1.OCILayerCopy, + } obj.Status.Artifact = &sourcev1.Artifact{ Revision: testRevision, } - obj.Status.ContentConfigChecksum = "sha256:fcfd705e10431a341f2df5b05ecee1fb54facd9a5e88b0be82276bdf533b6c64" }, afterFunc: func(g *WithT, artifact *sourcev1.Artifact) { g.Expect(artifact.Metadata).To(BeEmpty()) @@ -1336,10 +1364,13 @@ func TestOCIRepository_reconcileSource_noop(t *testing.T) { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Operation: sourcev1.OCILayerExtract, } + obj.Status.ObservedLayerSelector = &sourcev1.OCILayerSelector{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Operation: sourcev1.OCILayerCopy, + } obj.Status.Artifact = &sourcev1.Artifact{ Revision: testRevision, } - obj.Status.ContentConfigChecksum = "sha256:fcfd705e10431a341f2df5b05ecee1fb54facd9a5e88b0be82276bdf533b6c64" }, afterFunc: func(g *WithT, artifact *sourcev1.Artifact) { g.Expect(artifact.Metadata).ToNot(BeEmpty()) @@ -1449,7 +1480,6 @@ func TestOCIRepository_reconcileArtifact(t *testing.T) { obj.Status.Artifact = &sourcev1.Artifact{ Revision: "revision", } - obj.Status.ContentConfigChecksum = ociRepoEmptyContentConfigChecksum }, assertArtifact: &sourcev1.Artifact{ Revision: "revision", @@ -1467,14 +1497,13 @@ func TestOCIRepository_reconcileArtifact(t *testing.T) { beforeFunc: func(obj *sourcev1.OCIRepository) { obj.Status.Artifact = &sourcev1.Artifact{Revision: "revision"} obj.Spec.Ignore = pointer.String("aaa") - obj.Status.ContentConfigChecksum = ociRepoEmptyContentConfigChecksum }, want: sreconcile.ResultSuccess, assertPaths: []string{ "latest.tar.gz", }, afterFunc: func(g *WithT, obj *sourcev1.OCIRepository) { - g.Expect(obj.Status.ContentConfigChecksum).To(Equal("sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0")) + g.Expect(*obj.Status.ObservedIgnore).To(Equal("aaa")) }, assertConditions: []metav1.Condition{ *conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for digest"), @@ -1489,14 +1518,13 @@ func TestOCIRepository_reconcileArtifact(t *testing.T) { beforeFunc: func(obj *sourcev1.OCIRepository) { obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: "foo"} obj.Status.Artifact = &sourcev1.Artifact{Revision: "revision"} - obj.Status.ContentConfigChecksum = ociRepoEmptyContentConfigChecksum }, want: sreconcile.ResultSuccess, assertPaths: []string{ "latest.tar.gz", }, afterFunc: func(g *WithT, obj *sourcev1.OCIRepository) { - g.Expect(obj.Status.ContentConfigChecksum).To(Equal("sha256:82410edf339ab2945d97e26b92b6499e57156db63b94c17654b6ab97fbf86dbb")) + g.Expect(obj.Status.ObservedLayerSelector.MediaType).To(Equal("foo")) }, assertConditions: []metav1.Condition{ *conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for digest"), @@ -1515,14 +1543,14 @@ func TestOCIRepository_reconcileArtifact(t *testing.T) { Operation: sourcev1.OCILayerCopy, } obj.Status.Artifact = &sourcev1.Artifact{Revision: "revision"} - obj.Status.ContentConfigChecksum = ociRepoEmptyContentConfigChecksum }, want: sreconcile.ResultSuccess, assertPaths: []string{ "latest.tar.gz", }, afterFunc: func(g *WithT, obj *sourcev1.OCIRepository) { - g.Expect(obj.Status.ContentConfigChecksum).To(Equal("sha256:0e0e1c82f6403c8ee74fdf51349c8b5d98c508b5374c507c7ffb2e41dbc875df")) + g.Expect(obj.Status.ObservedLayerSelector.MediaType).To(Equal("foo")) + g.Expect(obj.Status.ObservedLayerSelector.Operation).To(Equal(sourcev1.OCILayerCopy)) }, assertConditions: []metav1.Condition{ *conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for digest"), @@ -1538,7 +1566,8 @@ func TestOCIRepository_reconcileArtifact(t *testing.T) { obj.Spec.Ignore = pointer.String("aaa") obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: "foo"} obj.Status.Artifact = &sourcev1.Artifact{Revision: "revision"} - obj.Status.ContentConfigChecksum = "sha256:0b56187b81cab6c3485583a46bec631f5ea08a1f69b769457f0e4aafb47884e3" + obj.Status.ObservedIgnore = pointer.String("aaa") + obj.Status.ObservedLayerSelector = &sourcev1.OCILayerSelector{MediaType: "foo"} }, want: sreconcile.ResultSuccess, assertArtifact: &sourcev1.Artifact{ @@ -2245,26 +2274,131 @@ func createTLSServer() (*httptest.Server, []byte, []byte, []byte, tls.Certificat return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err } -func TestOCIRepository_calculateContentConfigChecksum(t *testing.T) { - g := NewWithT(t) - obj := &sourcev1.OCIRepository{} - r := &OCIRepositoryReconciler{} +func TestOCISourceConfigChanged(t *testing.T) { + tests := []struct { + name string + spec sourcev1.OCIRepositorySpec + status sourcev1.OCIRepositoryStatus + want bool + }{ + { + name: "same ignore, no layer selector", + spec: sourcev1.OCIRepositorySpec{ + Ignore: pointer.String("nnn"), + }, + status: sourcev1.OCIRepositoryStatus{ + ObservedIgnore: pointer.String("nnn"), + }, + want: false, + }, + { + name: "different ignore, no layer selector", + spec: sourcev1.OCIRepositorySpec{ + Ignore: pointer.String("nnn"), + }, + status: sourcev1.OCIRepositoryStatus{ + ObservedIgnore: pointer.String("mmm"), + }, + want: true, + }, + { + name: "same ignore, same layer selector", + spec: sourcev1.OCIRepositorySpec{ + Ignore: pointer.String("nnn"), + LayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerExtract, + }, + }, + status: sourcev1.OCIRepositoryStatus{ + ObservedIgnore: pointer.String("nnn"), + ObservedLayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerExtract, + }, + }, + want: false, + }, + { + name: "same ignore, different layer selector operation", + spec: sourcev1.OCIRepositorySpec{ + Ignore: pointer.String("nnn"), + LayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerCopy, + }, + }, + status: sourcev1.OCIRepositoryStatus{ + ObservedIgnore: pointer.String("nnn"), + ObservedLayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerExtract, + }, + }, + want: true, + }, + { + name: "same ignore, different layer selector mediatype", + spec: sourcev1.OCIRepositorySpec{ + Ignore: pointer.String("nnn"), + LayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "bar", + Operation: sourcev1.OCILayerExtract, + }, + }, + status: sourcev1.OCIRepositoryStatus{ + ObservedIgnore: pointer.String("nnn"), + ObservedLayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerExtract, + }, + }, + want: true, + }, + { + name: "no ignore, same layer selector", + spec: sourcev1.OCIRepositorySpec{ + LayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerExtract, + }, + }, + status: sourcev1.OCIRepositoryStatus{ + ObservedLayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerExtract, + }, + }, + want: false, + }, + { + name: "no ignore, different layer selector", + spec: sourcev1.OCIRepositorySpec{ + LayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "bar", + Operation: sourcev1.OCILayerExtract, + }, + }, + status: sourcev1.OCIRepositoryStatus{ + ObservedLayerSelector: &sourcev1.OCILayerSelector{ + MediaType: "foo", + Operation: sourcev1.OCILayerExtract, + }, + }, + want: true, + }, + } - emptyChecksum := r.calculateContentConfigChecksum(obj) - g.Expect(emptyChecksum).To(Equal(ociRepoEmptyContentConfigChecksum)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) - // Ignore modified. - obj.Spec.Ignore = pointer.String("some-rule") - ignoreModChecksum := r.calculateContentConfigChecksum(obj) - g.Expect(emptyChecksum).ToNot(Equal(ignoreModChecksum)) + obj := &sourcev1.OCIRepository{ + Spec: tt.spec, + Status: tt.status, + } - // LayerSelector modified. - obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{ - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + g.Expect(ociSourceConfigChanged(obj)).To(Equal(tt.want)) + }) } - mediaTypeChecksum := r.calculateContentConfigChecksum(obj) - g.Expect(ignoreModChecksum).ToNot(Equal(mediaTypeChecksum)) - obj.Spec.LayerSelector.Operation = sourcev1.OCILayerCopy - layerCopyChecksum := r.calculateContentConfigChecksum(obj) - g.Expect(mediaTypeChecksum).ToNot(Equal(layerCopyChecksum)) } diff --git a/docs/api/source.md b/docs/api/source.md index 8c4eda2ee..af12fe9f9 100644 --- a/docs/api/source.md +++ b/docs/api/source.md @@ -2608,7 +2608,8 @@ string

(Appears on: -OCIRepositorySpec) +OCIRepositorySpec, +OCIRepositoryStatus)

OCILayerSelector specifies which layer should be extracted from an OCI Artifact

@@ -3014,6 +3015,34 @@ It has the format of <algo>:<checksum>, for example: +observedIgnore
+ +string + + + +(Optional) +

ObservedIgnore is the observed exclusion patterns used for constructing +the source artifact.

+ + + + +observedLayerSelector
+ + +OCILayerSelector + + + + +(Optional) +

ObservedLayerSelector is the observed layer selector used for constructing +the source artifact.

+ + + + ReconcileRequestStatus
diff --git a/docs/spec/v1beta2/ocirepositories.md b/docs/spec/v1beta2/ocirepositories.md index 76cc73866..0320e8e5a 100644 --- a/docs/spec/v1beta2/ocirepositories.md +++ b/docs/spec/v1beta2/ocirepositories.md @@ -868,6 +868,53 @@ configurations of the OCIRepository that indicate a change in source and records it in `.status.contentConfigChecksum`. This field is used to determine if the source artifact needs to be rebuilt. +**Deprecation Note:** `contentConfigChecksum` is no longer used and will be +removed in the next API version. The individual components used for generating +content configuration checksum now have explicit fields in the status. This +makes the observations used by the controller for making artifact rebuild +decisions more transparent and easier to debug. + +### Observed Ignore + +The source-controller reports an observed ignore in the OCIRepository's +`.status.observedIgnore`. The observed ignore is the latest `.spec.ignore` value +which resulted in a [ready state](#ready-ocirepository), or stalled due to error +it can not recover from without human intervention. The value is the same as the +[ignore in spec](#ignore). It indicates the ignore rules used in building the +current artifact in storage. It is also used by the controller to determine if +an artifact needs to be rebuilt. + +Example: +```yaml +status: + ... + observedIgnore: | + hpa.yaml + build + ... +``` + +### Observed Layer Selector + +The source-controller reports an observed layer selector in the OCIRepository's +`.status.observedLayerSelector`. The observed layer selector is the latest +`.spec.layerSelector` value which resulted in a [ready state](#ready-ocirepository), +or stalled due to error it can not recover from without human intervention. +The value is the same as the [layer selector in spec](#layer-selector). +It indicates the layer selection configuration used in building the current +artifact in storage. It is also used by the controller to determine if an +artifact needs to be rebuilt. + +Example: +```yaml +status: + ... + observedLayerSelector: + mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip + operation: copy + ... +``` + ### Observed Generation The source-controller reports an [observed generation][typical-status-properties]