OCIRepo: Add observed content config in status

Replace content config checksum with explicit artifact content 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 <darkowlzz@protonmail.com>
This commit is contained in:
Sunny 2022-09-30 17:52:26 +05:30
parent 70d9f126f9
commit 278a223bc6
7 changed files with 337 additions and 71 deletions

View File

@ -211,9 +211,22 @@ type OCIRepositoryStatus struct {
// be used to determine if the content configuration has changed and the
// artifact needs to be rebuilt.
// It has the format of `<algo>:<checksum>`, for example: `sha256:<checksum>`.
//
// Deprecated: Replaced with explicit fields for observed artifact content
// config in the status.
// +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"`
}

View File

@ -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
}

View File

@ -301,12 +301,14 @@ spec:
type: object
type: array
contentConfigChecksum:
description: 'ContentConfigChecksum is a checksum of all the configurations
description: "ContentConfigChecksum is a checksum of all the configurations
related to the content of the source artifact: - .spec.ignore -
.spec.layerSelector observed in .status.observedGeneration version
of the object. This can be used to determine if the content configuration
has changed and the artifact needs to be rebuilt. It has the format
of `<algo>:<checksum>`, for example: `sha256:<checksum>`.'
of `<algo>:<checksum>`, for example: `sha256:<checksum>`. \n Deprecated:
Replaced with explicit fields for observed artifact content config
in the status."
type: string
lastHandledReconcileAt:
description: LastHandledReconcileAt holds the value of the most recent
@ -317,6 +319,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.

View File

@ -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) && !ociContentConfigChanged(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) && !ociContentConfigChanged(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) && !ociContentConfigChanged(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
}
// ociContentConfigChanged evaluates the current spec with the observations
// of the artifact in the status to determine if artifact content configuration
// has changed and requires rebuilding the artifact.
func ociContentConfigChanged(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
}

View File

@ -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{}
emptyChecksum := r.calculateContentConfigChecksum(obj)
g.Expect(emptyChecksum).To(Equal(ociRepoEmptyContentConfigChecksum))
// Ignore modified.
obj.Spec.Ignore = pointer.String("some-rule")
ignoreModChecksum := r.calculateContentConfigChecksum(obj)
g.Expect(emptyChecksum).ToNot(Equal(ignoreModChecksum))
// LayerSelector modified.
obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
func TestOCIContentConfigChanged(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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
obj := &sourcev1.OCIRepository{
Spec: tt.spec,
Status: tt.status,
}
g.Expect(ociContentConfigChanged(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))
}

View File

@ -2608,7 +2608,8 @@ string
</h3>
<p>
(<em>Appears on:</em>
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>)
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>,
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryStatus">OCIRepositoryStatus</a>)
</p>
<p>OCILayerSelector specifies which layer should be extracted from an OCI Artifact</p>
<div class="md-typeset__scrollwrap">
@ -3010,6 +3011,36 @@ observed in .status.observedGeneration version of the object. This can
be used to determine if the content configuration has changed and the
artifact needs to be rebuilt.
It has the format of <code>&lt;algo&gt;:&lt;checksum&gt;</code>, for example: <code>sha256:&lt;checksum&gt;</code>.</p>
<p>Deprecated: Replaced with explicit fields for observed artifact content
config in the status.</p>
</td>
</tr>
<tr>
<td>
<code>observedIgnore</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ObservedIgnore is the observed exclusion patterns used for constructing
the source artifact.</p>
</td>
</tr>
<tr>
<td>
<code>observedLayerSelector</code><br>
<em>
<a href="#source.toolkit.fluxcd.io/v1beta2.OCILayerSelector">
OCILayerSelector
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ObservedLayerSelector is the observed layer selector used for constructing
the source artifact.</p>
</td>
</tr>
<tr>

View File

@ -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]