Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions image/copy/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ func (ic *imageCopier) copyBlobFromStream(ctx context.Context, srcReader io.Read
Cache: ic.c.blobInfoCache,
IsConfig: isConfig,
EmptyLayer: emptyLayer,
Digests: ic.c.options.digestOptions,
// CannotChangeDigestReason requires stream.info.Digest to always be set, and it is:
// If ic.cannotModifyManifestReason, stream.info was not modified since its initialization at the top of this
// function, and the caller is required to provide a digest.
CannotChangeDigestReason: ic.cannotModifyManifestReason,
}
if !isConfig {
options.LayerIndex = &layerIndex
Expand Down
16 changes: 16 additions & 0 deletions image/copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/docker/reference"
internalblobinfocache "go.podman.io/image/v5/internal/blobinfocache"
"go.podman.io/image/v5/internal/digests"
"go.podman.io/image/v5/internal/image"
"go.podman.io/image/v5/internal/imagedestination"
"go.podman.io/image/v5/internal/imagesource"
Expand Down Expand Up @@ -155,6 +156,15 @@ type Options struct {
// In oci-archive: destinations, this will set the create/mod/access timestamps in each tar entry
// (but not a timestamp of the created archive file).
DestinationTimestamp *time.Time

// FIXME:
// - this reference to an internal type is unusable from the outside even if we made the field public
// - what is the actual semantics? Right now it is probably “choices to use when writing to the destination”, TBD
// - anyway do we want to expose _all_ of the digests.Options tunables, or fewer?
// - … do we want to expose _more_ granularity than that?
// - (“must have at least sha512 integrity when reading”, what does “at least” mean for random pairs of algorithms?)
// - should some of this be in config files, maybe ever per-registry?
digestOptions digests.Options
}

// OptionCompressionVariant allows to supply information about
Expand Down Expand Up @@ -200,6 +210,12 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
if options == nil {
options = &Options{}
}
// FIXME: Currently, digestsOptions is not implemented at all, and exists in the codebase
// only to allow gradually building the feature set.
// After c/image/copy consistently implements it, provide a public digest options API of some kind.
optionsCopy := *options
optionsCopy.digestOptions = digests.CanonicalDefault()
options = &optionsCopy

if err := validateImageListSelection(options.ImageListSelection); err != nil {
return nil, err
Expand Down
7 changes: 6 additions & 1 deletion image/directory/directory_dest.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/internal/digests"
"go.podman.io/image/v5/internal/imagedestination/impl"
"go.podman.io/image/v5/internal/imagedestination/stubs"
"go.podman.io/image/v5/internal/private"
Expand Down Expand Up @@ -150,7 +151,11 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io.
}
}()

digester, stream := putblobdigest.DigestIfUnknown(stream, inputInfo)
algorithm, err := options.Digests.Choose(digests.Situation{Preexisting: inputInfo.Digest, CannotChangeAlgorithmReason: options.CannotChangeDigestReason})
if err != nil {
return private.UploadedBlob{}, err
}
digester, stream := putblobdigest.DigestIfAlgorithmUnknown(stream, inputInfo, algorithm)

// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
size, err := io.Copy(blobFile, stream)
Expand Down
8 changes: 8 additions & 0 deletions image/docker/docker_image_dest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/internal/blobinfocache"
"go.podman.io/image/v5/internal/digests"
"go.podman.io/image/v5/internal/imagedestination/impl"
"go.podman.io/image/v5/internal/imagedestination/stubs"
"go.podman.io/image/v5/internal/iolimits"
Expand Down Expand Up @@ -705,6 +706,11 @@ func (d *dockerImageDestination) putSignaturesToSigstoreAttachments(ctx context.
return errors.New("writing sigstore attachments is disabled by configuration")
}

digestOptions, err := digests.CanonicalDefault().WithDefault(digest.Canonical) // FIXME: This is bad and redundant, but we ultimately want the choice to be provided by the caller; and this way it shows up on audit searches for digest.Canonical.
if err != nil {
return err
}

ociManifest, err := d.c.getSigstoreAttachmentManifest(ctx, d.ref, manifestDigest)
if err != nil {
return err
Expand Down Expand Up @@ -760,6 +766,7 @@ func (d *dockerImageDestination) putSignaturesToSigstoreAttachments(ctx context.
IsConfig: false,
EmptyLayer: false,
LayerIndex: nil,
Digests: digestOptions,
})
if err != nil {
return err
Expand All @@ -781,6 +788,7 @@ func (d *dockerImageDestination) putSignaturesToSigstoreAttachments(ctx context.
IsConfig: true,
EmptyLayer: false,
LayerIndex: nil,
Digests: digestOptions,
})
if err != nil {
return err
Expand Down
146 changes: 146 additions & 0 deletions image/internal/digests/digests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Package digests provides an internal representation of users’ digest use preferences.
//
// Something like this _might_ be eventually made available as a public API:
// before doing so, carefully think whether the API should be modified before we commit to it.

package digests

import (
"errors"
"fmt"

"github.com/opencontainers/go-digest"
)

// Options records users’ preferences for used digest algorithm usage.
// It is a value type and can be copied using ordinary assignment.
//
// It can only be created using one of the provided constructors.
type Options struct {
initialized bool // To prevent uses that don’t call a public constructor; this is necessary to enforce the .Available() promise.

// If any of the fields below is set, it is guaranteed to be .Available().

mustUse digest.Algorithm // If not "", written digests must use this algorithm.
prefer digest.Algorithm // If not "", use this algorithm whenever possible.
defaultAlgo digest.Algorithm // If not "", use this algorithm if there is no reason to use anything else.
}

// CanonicalDefault is Options which default to using digest.Canonical if there is no reason to use a different algorithm
// (e.g. when there is no pre-existing digest).
//
// The configuration can be customized using .WithPreferred() or .WithDefault().
func CanonicalDefault() Options {
// This does not set .defaultAlgo so that .WithDefault() can be called (once).
return Options{
initialized: true,
}
}

// MustUse constructs Options which always use algo.
func MustUse(algo digest.Algorithm) (Options, error) {
// We don’t provide Options.WithMustUse because there is no other option that makes a difference
// once .mustUse is set.
if !algo.Available() {
return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String())
}
return Options{
initialized: true,
mustUse: algo,
}, nil
}

// WithPreferred returns a copy of o with a “preferred” algorithm set to algo.
// The preferred algorithm is used whenever possible (but if there is a strict requirement to use something else, it will be overridden).
func (o Options) WithPreferred(algo digest.Algorithm) (Options, error) {
if err := o.ensureInitialized(); err != nil {
return Options{}, err
}
if o.prefer != "" {
return Options{}, errors.New("digests.Options already have a 'prefer' algorithm configured")
}

if !algo.Available() {
return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String())
}
o.prefer = algo
return o, nil
}

// WithDefault returns a copy of o with a “default” algorithm set to algo.
// The default algorithm is used if there is no reason to use anything else (e.g. when there is no pre-existing digest).
func (o Options) WithDefault(algo digest.Algorithm) (Options, error) {
if err := o.ensureInitialized(); err != nil {
return Options{}, err
}
if o.defaultAlgo != "" {
return Options{}, errors.New("digests.Options already have a 'default' algorithm configured")
}

if !algo.Available() {
return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String())
}
o.defaultAlgo = algo
return o, nil
}

// ensureInitialized returns an error if o is not initialized.
func (o Options) ensureInitialized() error {
if !o.initialized {
return errors.New("internal error: use of uninitialized digests.Options")
}
return nil
}

// Situation records the context in which a digest is being chosen.
type Situation struct {
Preexisting digest.Digest // If not "", a pre-existing digest value (frequently one which is cheaper to use than others)
CannotChangeAlgorithmReason string // The reason why we must use Preexisting, or "" if we can use other algorithms.
}

// Choose chooses a digest algorithm based on the options and the situation.
func (o Options) Choose(s Situation) (digest.Algorithm, error) {
if err := o.ensureInitialized(); err != nil {
return "", err
}

if s.CannotChangeAlgorithmReason != "" && s.Preexisting == "" {
return "", fmt.Errorf("internal error: digests.Situation.CannotChangeAlgorithmReason is set but Preexisting is empty")
}

var choice digest.Algorithm // = what we want to use
switch {
case o.mustUse != "":
choice = o.mustUse
case s.CannotChangeAlgorithmReason != "":
choice = s.Preexisting.Algorithm()
if !choice.Available() {
return "", fmt.Errorf("existing digest uses unimplemented algorithm %s", choice)
}
case o.prefer != "":
choice = o.prefer
case s.Preexisting != "" && s.Preexisting.Algorithm().Available():
choice = s.Preexisting.Algorithm()
case o.defaultAlgo != "":
choice = o.defaultAlgo
default:
choice = digest.Canonical // We assume digest.Canonical is always available.
}

if s.CannotChangeAlgorithmReason != "" && choice != s.Preexisting.Algorithm() {
return "", fmt.Errorf("requested to always use digest algorithm %s but we cannot replace existing digest algorithm %s: %s",
choice, s.Preexisting.Algorithm(), s.CannotChangeAlgorithmReason)
}

return choice, nil
}

// MustUseSet returns an algorithm if o is set to always use a specific algorithm, "" if it is flexible.
func (o Options) MustUseSet() digest.Algorithm {
// We don’t do .ensureInitialized() because that would require an extra error value just for that.
// This should not be a part of any public API either way.
if o.mustUse != "" {
return o.mustUse
}
return ""
}
Loading
Loading