diff --git a/image/copy/copy.go b/image/copy/copy.go index eed5f8d96d..cc165ff711 100644 --- a/image/copy/copy.go +++ b/image/copy/copy.go @@ -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" @@ -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 @@ -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 diff --git a/image/internal/digests/digests.go b/image/internal/digests/digests.go new file mode 100644 index 0000000000..ca76ea3be7 --- /dev/null +++ b/image/internal/digests/digests.go @@ -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 "" +} diff --git a/image/internal/digests/digests_test.go b/image/internal/digests/digests_test.go new file mode 100644 index 0000000000..e5a98a851f --- /dev/null +++ b/image/internal/digests/digests_test.go @@ -0,0 +1,201 @@ +package digests + +import ( + "testing" + + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCanonicalDefault(t *testing.T) { + o := CanonicalDefault() + assert.Equal(t, Options{initialized: true}, o) +} + +func TestMustUse(t *testing.T) { + o, err := MustUse(digest.SHA512) + require.NoError(t, err) + assert.Equal(t, Options{ + initialized: true, + mustUse: digest.SHA512, + }, o) + + _, err = MustUse(digest.Algorithm("this is not a known algorithm")) + require.Error(t, err) +} + +func TestOptionsWithPreferred(t *testing.T) { + preferSHA512, err := CanonicalDefault().WithPreferred(digest.SHA512) + require.NoError(t, err) + assert.Equal(t, Options{ + initialized: true, + prefer: digest.SHA512, + }, preferSHA512) + + for _, c := range []struct { + base Options + algo digest.Algorithm + }{ + { // Uninitialized Options + base: Options{}, + algo: digest.SHA256, + }, + { // Unavailable algorithm + base: CanonicalDefault(), + algo: digest.Algorithm("this is not a known algorithm"), + }, + { // WithPreferred already called + base: preferSHA512, + algo: digest.SHA512, + }, + } { + _, err := c.base.WithPreferred(c.algo) + assert.Error(t, err) + } +} + +func TestOptionsWithDefault(t *testing.T) { + defaultSHA512, err := CanonicalDefault().WithDefault(digest.SHA512) + require.NoError(t, err) + assert.Equal(t, Options{ + initialized: true, + defaultAlgo: digest.SHA512, + }, defaultSHA512) + + for _, c := range []struct { + base Options + algo digest.Algorithm + }{ + { // Uninitialized Options + base: Options{}, + algo: digest.SHA256, + }, + { // Unavailable algorithm + base: CanonicalDefault(), + algo: digest.Algorithm("this is not a known algorithm"), + }, + { // WithDefault already called + base: defaultSHA512, + algo: digest.SHA512, + }, + } { + _, err := c.base.WithDefault(c.algo) + assert.Error(t, err) + } +} + +func TestOptionsChoose(t *testing.T) { + sha512Digest := digest.Digest("sha512:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + unknownDigest := digest.Digest("unknown:abcd1234") + + // The tests use sha512 = pre-existing if any; sha384 = primary configured; sha256 = supposedly irrelevant. + + mustSHA384, err := MustUse(digest.SHA384) + require.NoError(t, err) + mustSHA384, err = mustSHA384.WithPreferred(digest.SHA256) + require.NoError(t, err) + mustSHA384, err = mustSHA384.WithDefault(digest.SHA256) + require.NoError(t, err) + + preferSHA384, err := CanonicalDefault().WithPreferred(digest.SHA384) + require.NoError(t, err) + preferSHA384, err = preferSHA384.WithDefault(digest.SHA256) + require.NoError(t, err) + + defaultSHA384, err := CanonicalDefault().WithDefault(digest.SHA384) + require.NoError(t, err) + + cases := []struct { + opts Options + wantDefault digest.Algorithm + wantPreexisting digest.Algorithm // Pre-existing sha512 + wantCannotChange digest.Algorithm // Pre-existing sha512, CannotChange + wantUnavailable digest.Algorithm // Pre-existing unavailable + }{ + { + opts: Options{}, // uninitialized + wantDefault: "", + wantPreexisting: "", + wantCannotChange: "", + wantUnavailable: "", + }, + { + opts: mustSHA384, + wantDefault: digest.SHA384, + wantPreexisting: digest.SHA384, + wantCannotChange: "", + // Warning: We don’t generally _promise_ that unavailable digests are going to be silently ignored + // in these situations (e.g. we might still try to validate them when reading inputs). + wantUnavailable: digest.SHA384, + }, + { + opts: preferSHA384, + wantDefault: digest.SHA384, + wantPreexisting: digest.SHA384, + wantCannotChange: digest.SHA512, + wantUnavailable: digest.SHA384, + }, + { + opts: defaultSHA384, + wantDefault: digest.SHA384, + wantPreexisting: digest.SHA512, + wantCannotChange: digest.SHA512, + wantUnavailable: digest.SHA384, + }, + { + opts: CanonicalDefault(), + wantDefault: digest.SHA256, + wantPreexisting: digest.SHA512, + wantCannotChange: digest.SHA512, + wantUnavailable: digest.SHA256, + }, + } + for _, c := range cases { + run := func(s Situation, want digest.Algorithm) { + res, err := c.opts.Choose(s) + if want == "" { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, want, res) + } + } + + run(Situation{}, c.wantDefault) + run(Situation{Preexisting: sha512Digest}, c.wantPreexisting) + run(Situation{Preexisting: sha512Digest, CannotChangeAlgorithmReason: "test reason"}, c.wantCannotChange) + run(Situation{Preexisting: unknownDigest}, c.wantUnavailable) + + run(Situation{Preexisting: unknownDigest, CannotChangeAlgorithmReason: "test reason"}, "") + run(Situation{CannotChangeAlgorithmReason: "test reason"}, "") // CannotChangeAlgorithm with missing Preexisting + } +} + +func TestOptionsMustUseSet(t *testing.T) { + mustSHA512, err := MustUse(digest.SHA512) + require.NoError(t, err) + prefersSHA512, err := CanonicalDefault().WithPreferred(digest.SHA512) + require.NoError(t, err) + defaultSHA512, err := CanonicalDefault().WithDefault(digest.SHA512) + require.NoError(t, err) + for _, c := range []struct { + opts Options + want digest.Algorithm + }{ + { + opts: mustSHA512, + want: digest.SHA512, + }, + { + opts: prefersSHA512, + want: "", + }, + { + opts: defaultSHA512, + want: "", + }, + } { + assert.Equal(t, c.want, c.opts.MustUseSet()) + } +}