diff --git a/copy/copy.go b/copy/copy.go index 867ba73c7c..5b26abb092 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -137,6 +137,17 @@ type Options struct { // DestinationCtx.CompressionFormat is used exclusively, and blobs of other // compression algorithms are not reused. ForceCompressionFormat bool + + // ReportResolvedReference, if set, asks the destination transport to store + // a “resolved” (more detailed) reference to the created image + // into the value this option points to. + // What “resolved” means is transport-specific. + // Most transports don’t support this, and cause the value to be set to nil. + // + // For the containers-storage: transport, the reference contains an image ID, + // so that storage.ResolveReference returns exactly the created image. + // WARNING: It is unspecified whether the reference also contains a reference.Named element. + ReportResolvedReference *types.ImageReference } // OptionCompressionVariant allows to supply information about @@ -337,7 +348,13 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, } } - if err := c.dest.Commit(ctx, c.unparsedToplevel); err != nil { + if options.ReportResolvedReference != nil { + *options.ReportResolvedReference = nil // The default outcome, if not specifically supported by the transport. + } + if err := c.dest.CommitWithOptions(ctx, private.CommitOptions{ + UnparsedToplevel: c.unparsedToplevel, + ReportResolvedReference: options.ReportResolvedReference, + }); err != nil { return nil, fmt.Errorf("committing the finished image: %w", err) } diff --git a/directory/directory_dest.go b/directory/directory_dest.go index c9b3903185..000c8987d9 100644 --- a/directory/directory_dest.go +++ b/directory/directory_dest.go @@ -251,14 +251,11 @@ func (d *dirImageDestination) PutSignaturesWithFormat(ctx context.Context, signa return nil } -// Commit marks the process of storing the image as successful and asks for the image to be persisted. -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: -// - Uploaded data MAY be visible to others before Commit() is called -// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) -func (d *dirImageDestination) Commit(context.Context, types.UnparsedImage) error { +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *dirImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { return nil } diff --git a/docker/archive/dest.go b/docker/archive/dest.go index 632ee7c493..9e0d320071 100644 --- a/docker/archive/dest.go +++ b/docker/archive/dest.go @@ -34,16 +34,17 @@ func newImageDestination(sys *types.SystemContext, ref archiveReference) (privat writer = w closeWriter = true } - tarDest := tarfile.NewDestination(sys, writer.archive, ref.Transport().Name(), ref.ref) - if sys != nil && sys.DockerArchiveAdditionalTags != nil { - tarDest.AddRepoTags(sys.DockerArchiveAdditionalTags) - } - return &archiveImageDestination{ - Destination: tarDest, + d := &archiveImageDestination{ ref: ref, writer: writer, closeWriter: closeWriter, - }, nil + } + tarDest := tarfile.NewDestination(sys, writer.archive, ref.Transport().Name(), ref.ref, d.CommitWithOptions) + if sys != nil && sys.DockerArchiveAdditionalTags != nil { + tarDest.AddRepoTags(sys.DockerArchiveAdditionalTags) + } + d.Destination = tarDest + return d, nil } // Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, @@ -60,14 +61,11 @@ func (d *archiveImageDestination) Close() error { return nil } -// Commit marks the process of storing the image as successful and asks for the image to be persisted. -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: -// - Uploaded data MAY be visible to others before Commit() is called -// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) -func (d *archiveImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *archiveImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { d.writer.imageCommitted() if d.closeWriter { // We could do this only in .Close(), but failures in .Close() are much more likely to be diff --git a/docker/daemon/daemon_dest.go b/docker/daemon/daemon_dest.go index 9b880a2e76..4a59a6a610 100644 --- a/docker/daemon/daemon_dest.go +++ b/docker/daemon/daemon_dest.go @@ -56,16 +56,17 @@ func newImageDestination(ctx context.Context, sys *types.SystemContext, ref daem goroutineContext, goroutineCancel := context.WithCancel(ctx) go imageLoadGoroutine(goroutineContext, c, reader, statusChannel) - return &daemonImageDestination{ + d := &daemonImageDestination{ ref: ref, mustMatchRuntimeOS: mustMatchRuntimeOS, - Destination: tarfile.NewDestination(sys, archive, ref.Transport().Name(), namedTaggedRef), archive: archive, goroutineCancel: goroutineCancel, statusChannel: statusChannel, writer: writer, committed: false, - }, nil + } + d.Destination = tarfile.NewDestination(sys, archive, ref.Transport().Name(), namedTaggedRef, d.CommitWithOptions) + return d, nil } // imageLoadGoroutine accepts tar stream on reader, sends it to c, and reports error or success by writing to statusChannel @@ -146,7 +147,7 @@ func (d *daemonImageDestination) Close() error { // immediately, and hopefully, through terminating the sending which uses "Transfer-Encoding: chunked"" without sending // the terminating zero-length chunk, prevent the docker daemon from processing the tar stream at all. // Whether that works or not, closing the PipeWriter seems desirable in any case. - if err := d.writer.CloseWithError(errors.New("Aborting upload, daemonImageDestination closed without a previous .Commit()")); err != nil { + if err := d.writer.CloseWithError(errors.New("Aborting upload, daemonImageDestination closed without a previous .CommitWithOptions()")); err != nil { return err } } @@ -159,14 +160,11 @@ func (d *daemonImageDestination) Reference() types.ImageReference { return d.ref } -// Commit marks the process of storing the image as successful and asks for the image to be persisted. -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: -// - Uploaded data MAY be visible to others before Commit() is called -// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) -func (d *daemonImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *daemonImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { logrus.Debugf("docker-daemon: Closing tar stream") if err := d.archive.Close(); err != nil { return err diff --git a/docker/docker_image_dest.go b/docker/docker_image_dest.go index ed3d4a2c0b..f5e2bb61e8 100644 --- a/docker/docker_image_dest.go +++ b/docker/docker_image_dest.go @@ -923,13 +923,10 @@ func (d *dockerImageDestination) putSignaturesToAPIExtension(ctx context.Context return nil } -// Commit marks the process of storing the image as successful and asks for the image to be persisted. -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: -// - Uploaded data MAY be visible to others before Commit() is called -// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) -func (d *dockerImageDestination) Commit(context.Context, types.UnparsedImage) error { +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *dockerImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { return nil } diff --git a/docker/internal/tarfile/dest.go b/docker/internal/tarfile/dest.go index d5446840b3..f142346807 100644 --- a/docker/internal/tarfile/dest.go +++ b/docker/internal/tarfile/dest.go @@ -20,22 +20,25 @@ import ( "github.com/sirupsen/logrus" ) -// Destination is a partial implementation of private.ImageDestination for writing to an io.Writer. +// Destination is a partial implementation of private.ImageDestination for writing to a Writer. type Destination struct { impl.Compat impl.PropertyMethodsInitialize stubs.NoPutBlobPartialInitialize stubs.NoSignaturesInitialize - archive *Writer - repoTags []reference.NamedTagged + archive *Writer + commitWithOptions func(ctx context.Context, options private.CommitOptions) error + repoTags []reference.NamedTagged // Other state. config []byte sysCtx *types.SystemContext } // NewDestination returns a tarfile.Destination adding images to the specified Writer. -func NewDestination(sys *types.SystemContext, archive *Writer, transportName string, ref reference.NamedTagged) *Destination { +// commitWithOptions implements ImageDestination.CommitWithOptions. +func NewDestination(sys *types.SystemContext, archive *Writer, transportName string, ref reference.NamedTagged, + commitWithOptions func(ctx context.Context, options private.CommitOptions) error) *Destination { repoTags := []reference.NamedTagged{} if ref != nil { repoTags = append(repoTags, ref) @@ -57,9 +60,10 @@ func NewDestination(sys *types.SystemContext, archive *Writer, transportName str NoPutBlobPartialInitialize: stubs.NoPutBlobPartialRaw(transportName), NoSignaturesInitialize: stubs.NoSignatures("Storing signatures for docker tar files is not supported"), - archive: archive, - repoTags: repoTags, - sysCtx: sys, + archive: archive, + commitWithOptions: commitWithOptions, + repoTags: repoTags, + sysCtx: sys, } dest.Compat = impl.AddCompat(dest) return dest @@ -179,3 +183,13 @@ func (d *Destination) PutManifest(ctx context.Context, m []byte, instanceDigest return d.archive.ensureManifestItemLocked(man.LayersDescriptors, man.ConfigDescriptor.Digest, d.repoTags) } + +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. +// WARNING: This does not have any transactional semantics: +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *Destination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { + // This indirection exists because impl.Compat expects all ImageDestinationInternalOnly methods + // to be implemented in one place. + return d.commitWithOptions(ctx, options) +} diff --git a/docker/internal/tarfile/src_test.go b/docker/internal/tarfile/src_test.go index 1f8248ac56..97c312311b 100644 --- a/docker/internal/tarfile/src_test.go +++ b/docker/internal/tarfile/src_test.go @@ -28,7 +28,7 @@ func TestSourcePrepareLayerData(t *testing.T) { ctx := context.Background() writer := NewWriter(&tarfileBuffer) - dest := NewDestination(nil, writer, "transport name", nil) + dest := NewDestination(nil, writer, "transport name", nil, nil) // No layers configInfo, err := dest.PutBlob(ctx, strings.NewReader(c.config), types.BlobInfo{Size: -1}, cache, true) diff --git a/docker/tarfile/dest.go b/docker/tarfile/dest.go index 4547654396..2507d56004 100644 --- a/docker/tarfile/dest.go +++ b/docker/tarfile/dest.go @@ -6,6 +6,7 @@ import ( internal "github.com/containers/image/v5/docker/internal/tarfile" "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" ) @@ -25,10 +26,11 @@ func NewDestination(dest io.Writer, ref reference.NamedTagged) *Destination { // NewDestinationWithContext returns a tarfile.Destination for the specified io.Writer. func NewDestinationWithContext(sys *types.SystemContext, dest io.Writer, ref reference.NamedTagged) *Destination { archive := internal.NewWriter(dest) - return &Destination{ - internal: internal.NewDestination(sys, archive, "[An external docker/tarfile caller]", ref), - archive: archive, + d := &Destination{ + archive: archive, } + d.internal = internal.NewDestination(sys, archive, "[An external docker/tarfile caller]", ref, d.commitWithOptions) + return d } // AddRepoTags adds the specified tags to the destination's repoTags. @@ -117,3 +119,11 @@ func (d *Destination) PutSignatures(ctx context.Context, signatures [][]byte, in func (d *Destination) Commit(ctx context.Context) error { return d.archive.Close() } + +// commitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. +// WARNING: This does not have any transactional semantics: +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *Destination) commitWithOptions(ctx context.Context, options private.CommitOptions) error { + return d.Commit(ctx) +} diff --git a/internal/imagedestination/impl/compat.go b/internal/imagedestination/impl/compat.go index 47c169a1f8..70b207d9b5 100644 --- a/internal/imagedestination/impl/compat.go +++ b/internal/imagedestination/impl/compat.go @@ -99,3 +99,16 @@ func (c *Compat) PutSignatures(ctx context.Context, signatures [][]byte, instanc } return c.dest.PutSignaturesWithFormat(ctx, withFormat, instanceDigest) } + +// Commit marks the process of storing the image as successful and asks for the image to be persisted. +// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list +// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the +// original manifest list digest, if desired. +// WARNING: This does not have any transactional semantics: +// - Uploaded data MAY be visible to others before Commit() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) +func (c *Compat) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { + return c.dest.CommitWithOptions(ctx, private.CommitOptions{ + UnparsedToplevel: unparsedToplevel, + }) +} diff --git a/internal/imagedestination/wrapper.go b/internal/imagedestination/wrapper.go index f5a38541ae..0fe902e31f 100644 --- a/internal/imagedestination/wrapper.go +++ b/internal/imagedestination/wrapper.go @@ -97,3 +97,11 @@ func (w *wrapped) PutSignaturesWithFormat(ctx context.Context, signatures []sign } return w.PutSignatures(ctx, simpleSigs, instanceDigest) } + +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. +// WARNING: This does not have any transactional semantics: +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (w *wrapped) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { + return w.Commit(ctx, options.UnparsedToplevel) +} diff --git a/internal/private/private.go b/internal/private/private.go index 7390d91efa..4247a8db77 100644 --- a/internal/private/private.go +++ b/internal/private/private.go @@ -70,6 +70,12 @@ type ImageDestinationInternalOnly interface { // (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. // MUST be called after PutManifest (signatures may reference manifest contents). PutSignaturesWithFormat(ctx context.Context, signatures []signature.Signature, instanceDigest *digest.Digest) error + + // CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. + // WARNING: This does not have any transactional semantics: + // - Uploaded data MAY be visible to others before CommitWithOptions() is called + // - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) + CommitWithOptions(ctx context.Context, options CommitOptions) error } // ImageDestination is an internal extension to the types.ImageDestination @@ -146,6 +152,19 @@ type ReusedBlob struct { MatchedByTOCDigest bool // Whether the layer was reused/matched by TOC digest. Used only for UI purposes. } +// CommitOptions are used in CommitWithOptions +type CommitOptions struct { + // UnparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list + // if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the + // original manifest list digest, if desired. + UnparsedToplevel types.UnparsedImage + // ReportResolvedReference, if set, asks the transport to store a “resolved” (more detailed) reference to the created image + // into the value this option points to. + // What “resolved” means is transport-specific. + // Transports which don’t support reporting resolved references can ignore the field; the generic copy code writes "nil" into the value. + ReportResolvedReference *types.ImageReference +} + // ImageSourceChunk is a portion of a blob. // This API is experimental and can be changed without bumping the major version number. type ImageSourceChunk struct { diff --git a/oci/archive/oci_dest.go b/oci/archive/oci_dest.go index f9c05e39d7..1d1e26c84f 100644 --- a/oci/archive/oci_dest.go +++ b/oci/archive/oci_dest.go @@ -150,13 +150,12 @@ func (d *ociArchiveImageDestination) PutSignaturesWithFormat(ctx context.Context return d.unpackedDest.PutSignaturesWithFormat(ctx, signatures, instanceDigest) } -// Commit marks the process of storing the image as successful and asks for the image to be persisted -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. -// after the directory is made, it is tarred up into a file and the directory is deleted -func (d *ociArchiveImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { - if err := d.unpackedDest.Commit(ctx, unparsedToplevel); err != nil { +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. +// WARNING: This does not have any transactional semantics: +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *ociArchiveImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { + if err := d.unpackedDest.CommitWithOptions(ctx, options); err != nil { return fmt.Errorf("storing image %q: %w", d.ref.image, err) } diff --git a/oci/layout/oci_dest.go b/oci/layout/oci_dest.go index a096afe0f1..85374cecf0 100644 --- a/oci/layout/oci_dest.go +++ b/oci/layout/oci_dest.go @@ -278,14 +278,11 @@ func (d *ociImageDestination) addManifest(desc *imgspecv1.Descriptor) { d.index.Manifests = append(slices.Clone(d.index.Manifests), *desc) } -// Commit marks the process of storing the image as successful and asks for the image to be persisted. -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: -// - Uploaded data MAY be visible to others before Commit() is called -// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) -func (d *ociImageDestination) Commit(context.Context, types.UnparsedImage) error { +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { layoutBytes, err := json.Marshal(imgspecv1.ImageLayout{ Version: imgspecv1.ImageLayoutVersion, }) diff --git a/openshift/openshift_dest.go b/openshift/openshift_dest.go index 833e670941..61f69e93fd 100644 --- a/openshift/openshift_dest.go +++ b/openshift/openshift_dest.go @@ -236,13 +236,10 @@ func (d *openshiftImageDestination) PutSignaturesWithFormat(ctx context.Context, return nil } -// Commit marks the process of storing the image as successful and asks for the image to be persisted. -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: -// - Uploaded data MAY be visible to others before Commit() is called -// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) -func (d *openshiftImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { - return d.docker.Commit(ctx, unparsedToplevel) +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *openshiftImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { + return d.docker.CommitWithOptions(ctx, options) } diff --git a/ostree/ostree_dest.go b/ostree/ostree_dest.go index 951b5d0987..d4ebe413b4 100644 --- a/ostree/ostree_dest.go +++ b/ostree/ostree_dest.go @@ -435,7 +435,11 @@ func (d *ostreeImageDestination) PutSignaturesWithFormat(ctx context.Context, si return nil } -func (d *ostreeImageDestination) Commit(context.Context, types.UnparsedImage) error { +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. +// WARNING: This does not have any transactional semantics: +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *ostreeImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { runtime.LockOSThread() defer runtime.UnlockOSThread() diff --git a/pkg/blobcache/dest.go b/pkg/blobcache/dest.go index 0b229d5a9a..f7103d13eb 100644 --- a/pkg/blobcache/dest.go +++ b/pkg/blobcache/dest.go @@ -307,6 +307,10 @@ func (d *blobCacheDestination) PutSignaturesWithFormat(ctx context.Context, sign return d.destination.PutSignaturesWithFormat(ctx, signatures, instanceDigest) } -func (d *blobCacheDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { - return d.destination.Commit(ctx, unparsedToplevel) +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. +// WARNING: This does not have any transactional semantics: +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (d *blobCacheDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { + return d.destination.CommitWithOptions(ctx, options) } diff --git a/storage/storage_dest.go b/storage/storage_dest.go index 5612a2a286..51fac71e44 100644 --- a/storage/storage_dest.go +++ b/storage/storage_dest.go @@ -1124,26 +1124,23 @@ func (s *storageImageDestination) untrustedLayerDiffID(layerIndex int) (digest.D return s.untrustedDiffIDValues[layerIndex], nil } -// Commit marks the process of storing the image as successful and asks for the image to be persisted. -// unparsedToplevel contains data about the top-level manifest of the source (which may be a single-arch image or a manifest list -// if PutManifest was only called for the single-arch image with instanceDigest == nil), primarily to allow lookups by the -// original manifest list digest, if desired. +// CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: -// - Uploaded data MAY be visible to others before Commit() is called -// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) -func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { +// - Uploaded data MAY be visible to others before CommitWithOptions() is called +// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) +func (s *storageImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { // This function is outside of the scope of HasThreadSafePutBlob, so we don’t need to hold s.lock. if len(s.manifest) == 0 { - return errors.New("Internal error: storageImageDestination.Commit() called without PutManifest()") + return errors.New("Internal error: storageImageDestination.CommitWithOptions() called without PutManifest()") } - toplevelManifest, _, err := unparsedToplevel.Manifest(ctx) + toplevelManifest, _, err := options.UnparsedToplevel.Manifest(ctx) if err != nil { return fmt.Errorf("retrieving top-level manifest: %w", err) } // If the name we're saving to includes a digest, then check that the // manifests that we're about to save all either match the one from the - // unparsedToplevel, or match the digest in the name that we're using. + // options.UnparsedToplevel, or match the digest in the name that we're using. if s.imageRef.named != nil { if digested, ok := s.imageRef.named.(reference.Digested); ok { matches, err := manifest.MatchesDigest(s.manifest, digested.Digest()) @@ -1176,24 +1173,24 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t }, blob.Size); err != nil { return err } else if stopQueue { - return fmt.Errorf("Internal error: storageImageDestination.Commit(): commitLayer() not ready to commit for layer %q", blob.Digest) + return fmt.Errorf("Internal error: storageImageDestination.CommitWithOptions(): commitLayer() not ready to commit for layer %q", blob.Digest) } } var lastLayer string if len(layerBlobs) > 0 { // Zero-layer images rarely make sense, but it is technically possible, and may happen for non-image artifacts. prev, ok := s.indexToStorageID[len(layerBlobs)-1] if !ok { - return fmt.Errorf("Internal error: storageImageDestination.Commit(): previous layer %d hasn't been committed (lastLayer == nil)", len(layerBlobs)-1) + return fmt.Errorf("Internal error: storageImageDestination.CommitWithOptions(): previous layer %d hasn't been committed (lastLayer == nil)", len(layerBlobs)-1) } lastLayer = prev } // If one of those blobs was a configuration blob, then we can try to dig out the date when the image // was originally created, in case we're just copying it. If not, no harm done. - options := &storage.ImageOptions{} + imgOptions := &storage.ImageOptions{} if inspect, err := man.Inspect(s.getConfigBlob); err == nil && inspect.Created != nil { logrus.Debugf("setting image creation date to %s", inspect.Created) - options.CreationDate = *inspect.Created + imgOptions.CreationDate = *inspect.Created } // Set up to save the non-layer blobs as data items. Since we only share layers, they should all be in files, so @@ -1210,13 +1207,13 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t if err != nil { return fmt.Errorf("copying non-layer blob %q to image: %w", blob, err) } - options.BigData = append(options.BigData, storage.ImageBigDataOption{ + imgOptions.BigData = append(imgOptions.BigData, storage.ImageBigDataOption{ Key: blob.String(), Data: v, Digest: digest.Canonical.FromBytes(v), }) } - // Set up to save the unparsedToplevel's manifest if it differs from + // Set up to save the options.UnparsedToplevel's manifest if it differs from // the per-platform one, which is saved below. if len(toplevelManifest) != 0 && !bytes.Equal(toplevelManifest, s.manifest) { manifestDigest, err := manifest.Digest(toplevelManifest) @@ -1227,7 +1224,7 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t if err != nil { return err } - options.BigData = append(options.BigData, storage.ImageBigDataOption{ + imgOptions.BigData = append(imgOptions.BigData, storage.ImageBigDataOption{ Key: key, Data: toplevelManifest, Digest: manifestDigest, @@ -1240,19 +1237,19 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t if err != nil { return err } - options.BigData = append(options.BigData, storage.ImageBigDataOption{ + imgOptions.BigData = append(imgOptions.BigData, storage.ImageBigDataOption{ Key: key, Data: s.manifest, Digest: s.manifestDigest, }) - options.BigData = append(options.BigData, storage.ImageBigDataOption{ + imgOptions.BigData = append(imgOptions.BigData, storage.ImageBigDataOption{ Key: storage.ImageDigestBigDataKey, Data: s.manifest, Digest: s.manifestDigest, }) // Set up to save the signatures, if we have any. if len(s.signatures) > 0 { - options.BigData = append(options.BigData, storage.ImageBigDataOption{ + imgOptions.BigData = append(imgOptions.BigData, storage.ImageBigDataOption{ Key: "signatures", Data: s.signatures, Digest: digest.Canonical.FromBytes(s.signatures), @@ -1263,7 +1260,7 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t if err != nil { return err } - options.BigData = append(options.BigData, storage.ImageBigDataOption{ + imgOptions.BigData = append(imgOptions.BigData, storage.ImageBigDataOption{ Key: key, Data: signatures, Digest: digest.Canonical.FromBytes(signatures), @@ -1276,7 +1273,7 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t return fmt.Errorf("encoding metadata for image: %w", err) } if len(metadata) != 0 { - options.Metadata = string(metadata) + imgOptions.Metadata = string(metadata) } // Create the image record, pointing to the most-recently added layer. @@ -1288,7 +1285,7 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t } } oldNames := []string{} - img, err := s.imageRef.transport.store.CreateImage(intendedID, nil, lastLayer, "", options) + img, err := s.imageRef.transport.store.CreateImage(intendedID, nil, lastLayer, "", imgOptions) if err != nil { if !errors.Is(err, storage.ErrDuplicateID) { logrus.Debugf("error creating image: %q", err) @@ -1309,21 +1306,21 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t // sizes (tracked in the metadata) which might have already // been present with new values, when ideally we'd find a way // to merge them since they all apply to the same image - for _, data := range options.BigData { + for _, data := range imgOptions.BigData { if err := s.imageRef.transport.store.SetImageBigData(img.ID, data.Key, data.Data, manifest.Digest); err != nil { logrus.Debugf("error saving big data %q for image %q: %v", data.Key, img.ID, err) return fmt.Errorf("saving big data %q for image %q: %w", data.Key, img.ID, err) } } - if options.Metadata != "" { - if err := s.imageRef.transport.store.SetMetadata(img.ID, options.Metadata); err != nil { + if imgOptions.Metadata != "" { + if err := s.imageRef.transport.store.SetMetadata(img.ID, imgOptions.Metadata); err != nil { logrus.Debugf("error saving metadata for image %q: %v", img.ID, err) return fmt.Errorf("saving metadata for image %q: %w", img.ID, err) } - logrus.Debugf("saved image metadata %q", options.Metadata) + logrus.Debugf("saved image metadata %q", imgOptions.Metadata) } } else { - logrus.Debugf("created new image ID %q with metadata %q", img.ID, options.Metadata) + logrus.Debugf("created new image ID %q with metadata %q", img.ID, imgOptions.Metadata) } // Clean up the unfinished image on any error. @@ -1346,6 +1343,21 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t } logrus.Debugf("added name %q to image %q", name, img.ID) } + if options.ReportResolvedReference != nil { + // FIXME? This is using nil for the named reference. + // It would be better to also use s.imageRef.named, because that allows us to resolve to the right + // digest / manifest (and corresponding signatures). + // The problem with that is that resolving such a reference fails if the s.imageRef.named name is moved to a different image + // (because it is a tag that moved, or because we have pulled “the same” image for a different architecture). + // Right now (2024-11), ReportResolvedReference is only used in c/common/libimage, where the caller only extracts the image ID, + // so the name does not matter; to give us options, copy.Options.ReportResolvedReference is explicitly refusing to document + // whether the value contains a name. + resolved, err := newReference(s.imageRef.transport, nil, intendedID) + if err != nil { + return fmt.Errorf("creating a resolved reference for (%s, %s): %w", s.imageRef.StringWithinTransport(), intendedID, err) + } + *options.ReportResolvedReference = resolved + } commitSucceeded = true return nil