From 4ec51ca306217c8d92c4b3c1d2b979f3046c08bf Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 23 Sep 2022 17:00:23 +0300 Subject: [PATCH 1/3] Add option to copy the OCI layer to storage Add on optional field to the `OCIRepository.spec.layerSelector` called `operation` that accepts one of the following values: `extract` or `copy`. When the operation is set to `copy`, instead of extracting the compressed layer, the controller copies the compressed blob as it is to storage, thus keeping the original content unaltered. Signed-off-by: Stefan Prodan --- api/v1beta2/ocirepository_types.go | 23 +++++++ ...rce.toolkit.fluxcd.io_ocirepositories.yaml | 9 +++ controllers/ocirepository_controller.go | 67 ++++++++++++++++--- controllers/ocirepository_controller_test.go | 7 ++ docs/api/source.md | 15 +++++ 5 files changed, 111 insertions(+), 10 deletions(-) diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go index 1aa855ac..b1a13508 100644 --- a/api/v1beta2/ocirepository_types.go +++ b/api/v1beta2/ocirepository_types.go @@ -45,6 +45,12 @@ const ( // AzureOCIProvider provides support for OCI authentication using a Azure Service Principal, // Managed Identity or Shared Key. AzureOCIProvider string = "azure" + + // OCILayerExtract defines the operation type for extracting the content from an OCI artifact layer. + OCILayerExtract = "extract" + + // OCILayerCopy defines the operation type for copying the content from an OCI artifact layer. + OCILayerCopy = "copy" ) // OCIRepositorySpec defines the desired state of OCIRepository @@ -156,6 +162,14 @@ type OCILayerSelector struct { // first layer matching this type is selected. // +optional MediaType string `json:"mediaType,omitempty"` + + // 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. + // +kubebuilder:validation:Enum=extract;copy + // +optional + Operation string `json:"operation,omitempty"` } // OCIRepositoryVerification verifies the authenticity of an OCI Artifact @@ -231,6 +245,15 @@ func (in *OCIRepository) GetLayerMediaType() string { return in.Spec.LayerSelector.MediaType } +// GetLayerOperation returns the layer selector operation (defaults to extract). +func (in *OCIRepository) GetLayerOperation() string { + if in.Spec.LayerSelector == nil || in.Spec.LayerSelector.Operation == "" { + return OCILayerExtract + } + + return in.Spec.LayerSelector.Operation +} + // +genclient // +genclient:Namespaced // +kubebuilder:storageversion diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml index f4e94d19..a6c7ae40 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml @@ -90,6 +90,15 @@ spec: 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 provider: default: generic diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index 1003a574..023965f2 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -22,8 +22,10 @@ import ( "crypto/x509" "errors" "fmt" + "io" "net/http" "os" + "path/filepath" "sort" "strings" "time" @@ -499,6 +501,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour layer = layers[0] } + // Extract the compressed content from the selected layer blob, err := layer.Compressed() if err != nil { e := serror.NewGeneric( @@ -509,9 +512,42 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultEmpty, e } - if _, err = untar.Untar(blob, dir); err != nil { + // Persist layer content to storage using the specified operation + switch obj.GetLayerOperation() { + case sourcev1.OCILayerExtract: + if _, err = untar.Untar(blob, dir); err != nil { + e := serror.NewGeneric( + fmt.Errorf("failed to extract layer contents from artifact: %w", err), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + case sourcev1.OCILayerCopy: + metadata.Path = fmt.Sprintf("%s.tgz", metadata.Revision) + file, err := os.Create(filepath.Join(dir, metadata.Path)) + if err != nil { + e := serror.NewGeneric( + fmt.Errorf("failed to create file to copy layer to: %w", err), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + defer file.Close() + + _, err = io.Copy(file, blob) + if err != nil { + e := serror.NewGeneric( + fmt.Errorf("failed to copy layer from artifact: %w", err), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + default: e := serror.NewGeneric( - fmt.Errorf("failed to untar the first layer from artifact: %w", err), + fmt.Errorf("unsupported layer operation: %s", obj.GetLayerOperation()), sourcev1.OCILayerOperationFailedReason, ) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) @@ -915,14 +951,25 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, } defer unlock() - // Archive directory to storage - if err := r.Storage.Archive(&artifact, dir, nil); err != nil { - e := serror.NewGeneric( - fmt.Errorf("unable to archive artifact to storage: %s", err), - sourcev1.ArchiveOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e + switch obj.GetLayerOperation() { + case sourcev1.OCILayerCopy: + if err = r.Storage.CopyFromPath(&artifact, filepath.Join(dir, metadata.Path)); err != nil { + e := serror.NewGeneric( + fmt.Errorf("unable to copy artifact to storage: %w", err), + sourcev1.ArchiveOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + default: + if err := r.Storage.Archive(&artifact, dir, nil); err != nil { + e := serror.NewGeneric( + fmt.Errorf("unable to archive artifact to storage: %s", err), + sourcev1.ArchiveOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } } // Record it on the object diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index 9bd4aa77..aec8dcf4 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -85,6 +85,7 @@ func TestOCIRepository_Reconcile(t *testing.T) { semver string digest string mediaType string + operation string assertArtifact []artifactFixture }{ { @@ -93,6 +94,7 @@ func TestOCIRepository_Reconcile(t *testing.T) { tag: podinfoVersions["6.1.6"].tag, digest: podinfoVersions["6.1.6"].digest.Hex, mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + operation: sourcev1.OCILayerCopy, assertArtifact: []artifactFixture{ { expectedPath: "kustomize/deployment.yaml", @@ -150,7 +152,12 @@ func TestOCIRepository_Reconcile(t *testing.T) { } if tt.mediaType != "" { obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: tt.mediaType} + + if tt.operation != "" { + obj.Spec.LayerSelector.Operation = tt.operation + } } + g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace} diff --git a/docs/api/source.md b/docs/api/source.md index 9426f183..96b26b3e 100644 --- a/docs/api/source.md +++ b/docs/api/source.md @@ -2635,6 +2635,21 @@ which should be extracted from the OCI Artifact. The first layer matching this type is selected.

+ + +operation
+ +string + + + +(Optional) +

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.

+ + From aae9d917fbc9ae5eef1fb10b6d5cc8a2c4a03ec4 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Sat, 24 Sep 2022 10:30:46 +0300 Subject: [PATCH 2/3] Optimise OCI artifacts reconciliation - Fetch the upstream digest before validation and pulling - Pull artifact only if the upstream digest is different from the one in storage - Add the image tag to the revision string `/` for a better UX - Extract the layer processing to a dedicated function Signed-off-by: Stefan Prodan --- controllers/ocirepository_controller.go | 295 ++++++++++--------- controllers/ocirepository_controller_test.go | 36 +-- 2 files changed, 175 insertions(+), 156 deletions(-) diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index 023965f2..bd01a638 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -369,47 +369,18 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultEmpty, e } - // Pull artifact from the remote container registry - img, err := crane.Pull(url, options...) + // Get the upstream revision from the artifact digest + revision, err := r.getRevision(url, options) if err != nil { e := serror.NewGeneric( - fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err), + fmt.Errorf("failed to determine artifact digest: %w", err), sourcev1.OCIPullFailedReason, ) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) return sreconcile.ResultEmpty, e } - - // Determine the artifact SHA256 digest - imgDigest, err := img.Digest() - if err != nil { - e := serror.NewGeneric( - fmt.Errorf("failed to determine artifact digest: %w", err), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - - // Set the internal revision to the remote digest hex - revision := imgDigest.Hex - - // Copy the OCI annotations to the internal artifact metadata - manifest, err := img.Manifest() - if err != nil { - e := serror.NewGeneric( - fmt.Errorf("failed to parse artifact manifest: %w", err), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - - m := &sourcev1.Artifact{ - Revision: revision, - Metadata: manifest.Annotations, - } - m.DeepCopyInto(metadata) + metaArtifact := &sourcev1.Artifact{Revision: revision} + metaArtifact.DeepCopyInto(metadata) // Mark observations about the revision on the object defer func() { @@ -430,7 +401,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour } else if !obj.GetArtifact().HasRevision(revision) || conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation || conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) { - err := r.verifyOCISourceSignature(ctx, obj, url, keychain) + err := r.verifySignature(ctx, obj, url, keychain) if err != nil { provider := obj.Spec.Verify.Provider if obj.Spec.Verify.SecretRef == nil { @@ -447,121 +418,173 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest %s", revision) } - // Extract the content of the first artifact layer - if !obj.GetArtifact().HasRevision(revision) { - layers, err := img.Layers() + // Skip pulling if the artifact revision hasn't changes + if obj.GetArtifact().HasRevision(revision) { + conditions.Delete(obj, sourcev1.FetchFailedCondition) + return sreconcile.ResultSuccess, nil + } + + // Pull artifact from the remote container registry + img, err := crane.Pull(url, options...) + if err != nil { + e := serror.NewGeneric( + fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err), + sourcev1.OCIPullFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + + // Copy the OCI annotations to the internal artifact metadata + manifest, err := img.Manifest() + if err != nil { + e := serror.NewGeneric( + fmt.Errorf("failed to parse artifact manifest: %w", err), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + metadata.Metadata = manifest.Annotations + + // Extract the compressed content from the selected layer + blob, err := r.getLayerCompressed(obj, img) + if err != nil { + e := serror.NewGeneric(err, sourcev1.OCILayerOperationFailedReason) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + + // Persist layer content to storage using the specified operation + switch obj.GetLayerOperation() { + case sourcev1.OCILayerExtract: + if _, err = untar.Untar(blob, dir); err != nil { + e := serror.NewGeneric( + fmt.Errorf("failed to extract layer contents from artifact: %w", err), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + case sourcev1.OCILayerCopy: + metadata.Path = fmt.Sprintf("%s.tgz", r.digestFromRevision(metadata.Revision)) + file, err := os.Create(filepath.Join(dir, metadata.Path)) if err != nil { e := serror.NewGeneric( - fmt.Errorf("failed to parse artifact layers: %w", err), + fmt.Errorf("failed to create file to copy layer to: %w", err), sourcev1.OCILayerOperationFailedReason, ) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) return sreconcile.ResultEmpty, e } + defer file.Close() - if len(layers) < 1 { - e := serror.NewGeneric( - fmt.Errorf("no layers found in artifact"), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - - var layer gcrv1.Layer - - switch { - case obj.GetLayerMediaType() != "": - var found bool - for i, l := range layers { - md, err := l.MediaType() - if err != nil { - e := serror.NewGeneric( - fmt.Errorf("failed to determine the media type of layer[%v] from artifact: %w", i, err), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - if string(md) == obj.GetLayerMediaType() { - layer = layers[i] - found = true - break - } - } - if !found { - e := serror.NewGeneric( - fmt.Errorf("failed to find layer with media type '%s' in artifact", obj.GetLayerMediaType()), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - default: - layer = layers[0] - } - - // Extract the compressed content from the selected layer - blob, err := layer.Compressed() + _, err = io.Copy(file, blob) if err != nil { e := serror.NewGeneric( - fmt.Errorf("failed to extract the first layer from artifact: %w", err), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - - // Persist layer content to storage using the specified operation - switch obj.GetLayerOperation() { - case sourcev1.OCILayerExtract: - if _, err = untar.Untar(blob, dir); err != nil { - e := serror.NewGeneric( - fmt.Errorf("failed to extract layer contents from artifact: %w", err), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - case sourcev1.OCILayerCopy: - metadata.Path = fmt.Sprintf("%s.tgz", metadata.Revision) - file, err := os.Create(filepath.Join(dir, metadata.Path)) - if err != nil { - e := serror.NewGeneric( - fmt.Errorf("failed to create file to copy layer to: %w", err), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - defer file.Close() - - _, err = io.Copy(file, blob) - if err != nil { - e := serror.NewGeneric( - fmt.Errorf("failed to copy layer from artifact: %w", err), - sourcev1.OCILayerOperationFailedReason, - ) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - default: - e := serror.NewGeneric( - fmt.Errorf("unsupported layer operation: %s", obj.GetLayerOperation()), + fmt.Errorf("failed to copy layer from artifact: %w", err), sourcev1.OCILayerOperationFailedReason, ) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) return sreconcile.ResultEmpty, e } + default: + e := serror.NewGeneric( + fmt.Errorf("unsupported layer operation: %s", obj.GetLayerOperation()), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e } conditions.Delete(obj, sourcev1.FetchFailedCondition) return sreconcile.ResultSuccess, nil } -// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries using a key +// getLayerCompressed finds the matching layer and returns its compress contents +func (r *OCIRepositoryReconciler) getLayerCompressed(obj *sourcev1.OCIRepository, image gcrv1.Image) (io.ReadCloser, error) { + layers, err := image.Layers() + if err != nil { + return nil, fmt.Errorf("failed to parse artifact layers: %w", err) + } + + if len(layers) < 1 { + return nil, fmt.Errorf("no layers found in artifact") + } + + var layer gcrv1.Layer + switch { + case obj.GetLayerMediaType() != "": + var found bool + for i, l := range layers { + md, err := l.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to determine the media type of layer[%v] from artifact: %w", i, err) + } + if string(md) == obj.GetLayerMediaType() { + layer = layers[i] + found = true + break + } + } + if !found { + return nil, fmt.Errorf("failed to find layer with media type '%s' in artifact", obj.GetLayerMediaType()) + } + default: + layer = layers[0] + } + + blob, err := layer.Compressed() + if err != nil { + return nil, fmt.Errorf("failed to extract the first layer from artifact: %w", err) + } + + return blob, nil +} + +// getRevision fetches the upstream digest and returns the revision in the format `/` +func (r *OCIRepositoryReconciler) getRevision(url string, options []crane.Option) (string, error) { + ref, err := name.ParseReference(url) + if err != nil { + return "", err + } + + repoTag := "" + repoName := strings.TrimPrefix(url, ref.Context().RegistryStr()) + if s := strings.Split(repoName, ":"); len(s) == 2 && !strings.Contains(repoName, "@") { + repoTag = s[1] + } + + if repoTag == "" && !strings.Contains(repoName, "@") { + repoTag = "latest" + } + + digest, err := crane.Digest(url, options...) + if err != nil { + return "", err + } + + digestHash, err := gcrv1.NewHash(digest) + if err != nil { + return "", err + } + + revision := digestHash.Hex + if repoTag != "" { + revision = fmt.Sprintf("%s/%s", repoTag, digestHash.Hex) + } + return revision, nil +} + +// digestFromRevision extract the digest from the revision string +func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string { + parts := strings.Split(revision, "/") + return parts[len(parts)-1] +} + +// verifySignature verifies the authenticity of the given image reference url. First, it tries using a key // if a secret with a valid public key is provided. If not, it falls back to a keyless approach for verification. -func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error { +func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error { ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) defer cancel() @@ -856,8 +879,7 @@ func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context, insecure boo // condition is added. // The hostname of any URL in the Status of the object are updated, to ensure // they match the Storage server hostname of current runtime. -func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context, - obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) { +func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context, obj *sourcev1.OCIRepository, _ *sourcev1.Artifact, _ string) (sreconcile.Result, error) { // Garbage collect previous advertised artifact(s) from storage _ = r.garbageCollect(ctx, obj) @@ -892,13 +914,12 @@ func (r *OCIRepositoryReconciler) reconcileStorage(ctx context.Context, // early. // On a successful archive, the Artifact in the Status of the object is set, // and the symlink in the Storage is updated to its path. -func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, - obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) { - // Calculate revision +func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) { revision := metadata.Revision // Create artifact - artifact := r.Storage.NewArtifactFor(obj.Kind, obj, revision, fmt.Sprintf("%s.tar.gz", revision)) + artifact := r.Storage.NewArtifactFor(obj.Kind, obj, revision, + fmt.Sprintf("%s.tar.gz", r.digestFromRevision(revision))) // Set the ArtifactInStorageCondition if there's no drift. defer func() { @@ -1047,8 +1068,7 @@ func (r *OCIRepositoryReconciler) garbageCollect(ctx context.Context, obj *sourc // This log is different from the debug log in the EventRecorder, in the sense // that this is a simple log. While the debug log contains complete details // about the event. -func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context, - obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) { +func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context, obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) { msg := fmt.Sprintf(messageFmt, args...) // Log and emit event. if eventType == corev1.EventTypeWarning { @@ -1060,8 +1080,7 @@ func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context, } // notify emits notification related to the reconciliation. -func (r *OCIRepositoryReconciler) notify(ctx context.Context, - oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) { +func (r *OCIRepositoryReconciler) notify(ctx context.Context, oldObj, newObj *sourcev1.OCIRepository, res sreconcile.Result, resErr error) { // Notify successful reconciliation for new artifact and recovery from any // failure. if resErr == nil && res == sreconcile.ResultSuccess && newObj.Status.Artifact != nil { diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index aec8dcf4..82af8475 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -92,7 +92,7 @@ func TestOCIRepository_Reconcile(t *testing.T) { name: "public tag", url: podinfoVersions["6.1.6"].url, tag: podinfoVersions["6.1.6"].tag, - digest: podinfoVersions["6.1.6"].digest.Hex, + digest: fmt.Sprintf("%s/%s", podinfoVersions["6.1.6"].tag, podinfoVersions["6.1.6"].digest.Hex), mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", operation: sourcev1.OCILayerCopy, assertArtifact: []artifactFixture{ @@ -110,7 +110,7 @@ func TestOCIRepository_Reconcile(t *testing.T) { name: "public semver", url: podinfoVersions["6.1.5"].url, semver: ">= 6.1 <= 6.1.5", - digest: podinfoVersions["6.1.5"].digest.Hex, + digest: fmt.Sprintf("%s/%s", podinfoVersions["6.1.5"].tag, podinfoVersions["6.1.5"].digest.Hex), assertArtifact: []artifactFixture{ { expectedPath: "kustomize/deployment.yaml", @@ -449,7 +449,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { }), }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact from "), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to determine artifact digest"), }, }, { @@ -470,7 +470,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { includeSecret: true, }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact from "), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "UNAUTHORIZED"), }, }, { @@ -491,7 +491,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { includeSA: true, }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact from "), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "UNAUTHORIZED"), }, }, { @@ -533,7 +533,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { }), }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact from "), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to determine artifact digest"), }, }, { @@ -558,7 +558,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { }, }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact from "), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to determine artifact digest"), }, }, { @@ -683,7 +683,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { assertConditions := tt.assertConditions for k := range assertConditions { - assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", img.digest.Hex) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", fmt.Sprintf("%s/%s", img.tag, img.digest.Hex)) assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", repoURL) } @@ -871,7 +871,7 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { { name: "no reference (latest tag)", want: sreconcile.ResultSuccess, - wantRevision: img6.digest.Hex, + wantRevision: fmt.Sprintf("latest/%s", img6.digest.Hex), assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), @@ -883,7 +883,7 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { Tag: "6.1.6", }, want: sreconcile.ResultSuccess, - wantRevision: img6.digest.Hex, + wantRevision: fmt.Sprintf("%s/%s", img6.tag, img6.digest.Hex), assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), @@ -895,7 +895,7 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { SemVer: ">= 6.1.5", }, want: sreconcile.ResultSuccess, - wantRevision: img6.digest.Hex, + wantRevision: fmt.Sprintf("%s/%s", img6.tag, img6.digest.Hex), assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), @@ -921,7 +921,7 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { want: sreconcile.ResultEmpty, wantErr: true, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact"), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, " MANIFEST_UNKNOWN"), }, }, { @@ -943,7 +943,7 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { want: sreconcile.ResultEmpty, wantErr: true, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact"), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to determine artifact digest"), }, }, { @@ -953,7 +953,7 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { Tag: "6.1.5", }, want: sreconcile.ResultSuccess, - wantRevision: img6.digest.Hex, + wantRevision: fmt.Sprintf("%s/%s", img6.tag, img6.digest.Hex), assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), @@ -1091,7 +1091,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { beforeFunc: func(obj *sourcev1.OCIRepository) { conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, "VerifyFailed", "fail msg") obj.Spec.Verify = nil - obj.Status.Artifact = &sourcev1.Artifact{Revision: img4.digest.Hex} + obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s/%s", img4.tag, img4.digest.Hex)} }, want: sreconcile.ResultSuccess, }, @@ -1101,7 +1101,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { digest: img4.digest.Hex, shouldSign: true, beforeFunc: func(obj *sourcev1.OCIRepository) { - obj.Status.Artifact = &sourcev1.Artifact{Revision: img4.digest.Hex} + obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s/%s", img4.tag, img4.digest.Hex)} // Set Verified with old observed generation and different reason/message. conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified") // Set new object generation. @@ -1119,7 +1119,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { shouldSign: true, beforeFunc: func(obj *sourcev1.OCIRepository) { // Artifact present and custom verified condition reason/message. - obj.Status.Artifact = &sourcev1.Artifact{Revision: img4.digest.Hex} + obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s/%s", img4.tag, img4.digest.Hex)} conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified") }, want: sreconcile.ResultSuccess, @@ -1213,7 +1213,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { assertConditions := tt.assertConditions for k := range assertConditions { - assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", tt.digest) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", fmt.Sprintf("%s/%s", tt.reference.Tag, tt.digest)) assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", artifactURL) assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", "cosign") } From 3f7d4630cc8e86646f19d3b464728bc4be2ac8a3 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 26 Sep 2022 13:05:27 +0300 Subject: [PATCH 3/3] Use the OCI artifact revision in status and events Signed-off-by: Stefan Prodan --- controllers/ocirepository_controller.go | 15 +++-- controllers/ocirepository_controller_test.go | 68 ++++++++++---------- docs/spec/v1beta2/ocirepositories.md | 25 ++++--- 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index bd01a638..95ec1ec6 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -385,7 +385,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour // Mark observations about the revision on the object defer func() { if !obj.GetArtifact().HasRevision(revision) { - message := fmt.Sprintf("new digest '%s' for '%s'", revision, url) + message := fmt.Sprintf("new revision '%s' for '%s'", revision, url) conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", message) conditions.MarkReconciling(obj, "NewRevision", message) } @@ -415,7 +415,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultEmpty, e } - conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest %s", revision) + conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision %s", revision) } // Skip pulling if the artifact revision hasn't changes @@ -448,7 +448,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour metadata.Metadata = manifest.Annotations // Extract the compressed content from the selected layer - blob, err := r.getLayerCompressed(obj, img) + blob, err := r.selectLayer(obj, img) if err != nil { e := serror.NewGeneric(err, sourcev1.OCILayerOperationFailedReason) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) @@ -501,8 +501,9 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultSuccess, nil } -// getLayerCompressed finds the matching layer and returns its compress contents -func (r *OCIRepositoryReconciler) getLayerCompressed(obj *sourcev1.OCIRepository, image gcrv1.Image) (io.ReadCloser, error) { +// selectLayer finds the matching layer and returns its compressed contents. +// If no layer selector was provided, we pick the first layer from the OCI artifact. +func (r *OCIRepositoryReconciler) selectLayer(obj *sourcev1.OCIRepository, image gcrv1.Image) (io.ReadCloser, error) { layers, err := image.Layers() if err != nil { return nil, fmt.Errorf("failed to parse artifact layers: %w", err) @@ -933,7 +934,7 @@ func (r *OCIRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *so // The artifact is up-to-date if obj.GetArtifact().HasRevision(artifact.Revision) { r.eventLogf(ctx, obj, events.EventTypeTrace, sourcev1.ArtifactUpToDateReason, - "artifact up-to-date with remote digest: '%s'", artifact.Revision) + "artifact up-to-date with remote revision: '%s'", artifact.Revision) return sreconcile.ResultSuccess, nil } @@ -1094,7 +1095,7 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context, oldObj, newObj *so oldChecksum = oldObj.GetArtifact().Checksum } - message := fmt.Sprintf("stored artifact with digest '%s' from '%s'", newObj.Status.Artifact.Revision, newObj.Spec.URL) + message := fmt.Sprintf("stored artifact with revision '%s' from '%s'", newObj.Status.Artifact.Revision, newObj.Spec.URL) // enrich message with upstream annotations if found if info := newObj.GetArtifact().Metadata; info != nil { diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index 82af8475..7449531c 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -390,8 +390,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { name: "HTTP without basic auth", want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), }, }, { @@ -411,8 +411,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { includeSecret: true, }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), }, }, { @@ -432,8 +432,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { includeSA: true, }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), }, }, { @@ -515,8 +515,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { }, }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), }, }, { @@ -587,8 +587,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { }, provider: "azure", assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), }, }, } @@ -873,8 +873,8 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { want: sreconcile.ResultSuccess, wantRevision: fmt.Sprintf("latest/%s", img6.digest.Hex), assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision"), }, }, { @@ -885,8 +885,8 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { want: sreconcile.ResultSuccess, wantRevision: fmt.Sprintf("%s/%s", img6.tag, img6.digest.Hex), assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision"), }, }, { @@ -897,8 +897,8 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { want: sreconcile.ResultSuccess, wantRevision: fmt.Sprintf("%s/%s", img6.tag, img6.digest.Hex), assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision"), }, }, { @@ -909,8 +909,8 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { wantRevision: img6.digest.Hex, want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision"), }, }, { @@ -955,8 +955,8 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { want: sreconcile.ResultSuccess, wantRevision: fmt.Sprintf("%s/%s", img6.tag, img6.digest.Hex), assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision"), }, }, { @@ -969,8 +969,8 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { want: sreconcile.ResultSuccess, wantRevision: img5.digest.Hex, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision"), }, }, } @@ -1049,9 +1049,9 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { shouldSign: true, want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest "), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision "), }, }, { @@ -1064,8 +1064,8 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { wantErrMsg: "failed to verify the signature using provider 'cosign': no matching signatures were found for ''", want: sreconcile.ResultEmpty, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '': no matching signatures were found for ''"), }, }, @@ -1079,8 +1079,8 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { want: sreconcile.ResultEmpty, keyless: true, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '' for ''"), - *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '' for ''"), + *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '' for ''"), + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '' for ''"), *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider ' keyless': no matching signatures"), }, }, @@ -1109,7 +1109,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { }, want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest "), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision "), }, }, { @@ -1258,7 +1258,7 @@ func TestOCIRepository_reconcileArtifact(t *testing.T) { Revision: "revision", }, beforeFunc: func(obj *sourcev1.OCIRepository) { - conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest") + conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision") }, want: sreconcile.ResultSuccess, assertPaths: []string{ @@ -1698,7 +1698,7 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) { }, } }, - wantEvent: "Normal NewArtifact stored artifact with digest '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'", }, { name: "recovery from failure", @@ -1714,7 +1714,7 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) { obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Checksum: "yyy"} conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready") }, - wantEvent: "Normal Succeeded stored artifact with digest 'xxx' from 'oci://newurl.io'", + wantEvent: "Normal Succeeded stored artifact with revision 'xxx' from 'oci://newurl.io'", }, { name: "recovery and new artifact", @@ -1730,7 +1730,7 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) { obj.Status.Artifact = &sourcev1.Artifact{Revision: "aaa", Checksum: "bbb"} conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready") }, - wantEvent: "Normal NewArtifact stored artifact with digest 'aaa' from 'oci://newurl.io'", + wantEvent: "Normal NewArtifact stored artifact with revision 'aaa' from 'oci://newurl.io'", }, { name: "no updates", diff --git a/docs/spec/v1beta2/ocirepositories.md b/docs/spec/v1beta2/ocirepositories.md index 9e2e5069..76cc7386 100644 --- a/docs/spec/v1beta2/ocirepositories.md +++ b/docs/spec/v1beta2/ocirepositories.md @@ -31,7 +31,7 @@ In the above example: by the `.spec.interval` field. - It pulls the `latest` tag of the `ghcr.io/stefanprodan/manifests/podinfo` repository, indicated by the `.spec.ref.tag` and `.spec.url` fields. -- The resolved SHA256 digest is used as the Artifact +- The resolved tag and SHA256 digest is used as the Artifact revision, reported in-cluster in the `.status.artifact.revision` field. - When the current OCIRepository digest differs from the latest fetched digest, a new Artifact is archived. @@ -49,7 +49,7 @@ You can run this example by saving the manifest into `ocirepository.yaml`. ```console NAME URL AGE READY STATUS - podinfo oci://ghcr.io/stefanprodan/manifests/podinfo 5s True stored artifact with digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' + podinfo oci://ghcr.io/stefanprodan/manifests/podinfo 5s True stored artifact with revision 'latest/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' ``` 3. Run `kubectl describe ocirepository podinfo` to see the [Artifact](#artifact) @@ -62,17 +62,17 @@ You can run this example by saving the manifest into `ocirepository.yaml`. Checksum: d7e924b4882e55b97627355c7b3d2e711e9b54303afa2f50c25377f4df66a83b Last Update Time: 2022-06-14T11:23:36Z Path: ocirepository/default/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz - Revision: 3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de + Revision: latest/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de URL: http://source-controller.flux-system.svc.cluster.local./ocirepository/oci/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz Conditions: Last Transition Time: 2022-06-14T11:23:36Z - Message: stored artifact for digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' + Message: stored artifact for revision 'latest/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' Observed Generation: 1 Reason: Succeeded Status: True Type: Ready Last Transition Time: 2022-06-14T11:23:36Z - Message: stored artifact for digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' + Message: stored artifact for revision 'latest/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' Observed Generation: 1 Reason: Succeeded Status: True @@ -82,7 +82,7 @@ You can run this example by saving the manifest into `ocirepository.yaml`. Events: Type Reason Age From Message ---- ------ ---- ---- ------- - Normal NewArtifact 62s source-controller stored artifact with digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' from 'oci://ghcr.io/stefanprodan/manifests/podinfo' + Normal NewArtifact 62s source-controller stored artifact with revision 'latest/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' from 'oci://ghcr.io/stefanprodan/manifests/podinfo' ``` ## Writing an OCIRepository spec @@ -391,6 +391,7 @@ metadata: spec: layerSelector: mediaType: "application/deployment.content.v1.tar+gzip" + operation: extract # can be 'extract' or 'copy', defaults to 'extract' ``` If the layer selector matches more than one layer, the first layer matching the specified media type will be used. @@ -398,6 +399,10 @@ Note that the selected OCI layer must be [compressed](https://github.com/opencontainers/image-spec/blob/v1.0.2/layer.md#gzip-media-types) in the `tar+gzip` format. +When `.spec.layerSelector.operation` is set to `copy`, instead of extracting the +compressed layer, the controller copies the tarball as-is to storage, thus +keeping the original content unaltered. + ### Ignore `.spec.ignore` is an optional field to specify rules in [the `.gitignore` @@ -673,8 +678,8 @@ lists ```console LAST SEEN TYPE REASON OBJECT MESSAGE -2m14s Normal NewArtifact ocirepository/ stored artifact for digest '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' -36s Normal ArtifactUpToDate ocirepository/ artifact up-to-date with remote digest: '3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' +2m14s Normal NewArtifact ocirepository/ stored artifact for revision 'latest/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' +36s Normal ArtifactUpToDate ocirepository/ artifact up-to-date with remote revision: 'latest/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' 94s Warning OCIOperationFailed ocirepository/ failed to pull artifact from 'oci://ghcr.io/stefanprodan/manifests/podinfo': couldn't find tag "0.0.1" ``` @@ -690,7 +695,7 @@ specific OCIRepository, e.g. The OCIRepository reports the latest synchronized state from the OCI repository as an Artifact object in the `.status.artifact` of the resource. -The `.status.artifact.revision` holds the SHA256 digest of the upstream OCI artifact. +The `.status.artifact.revision` holds the tag and SHA256 digest of the upstream OCI artifact. The `.status.artifact.metadata` holds the upstream OCI artifact metadata such as the [OpenContainers standard annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md). @@ -719,7 +724,7 @@ status: org.opencontainers.image.revision: 6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872 org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git path: ocirepository///.tar.gz - revision: + revision: / url: http://source-controller..svc.cluster.local./ocirepository///.tar.gz ```