Skip to content

Commit a5aae3f

Browse files
committed
Add internal/digests.Options and internal/digests.Options.Choose
The goal is to allow buildling the digest-choice-dependent machinery from the bottom up without committing to an API while we don't understand the problem space; for now, we don't expose any way for users to actually make a choice. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
1 parent fb1cbee commit a5aae3f

File tree

2 files changed

+347
-0
lines changed

2 files changed

+347
-0
lines changed

image/internal/digests/digests.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// digests is an internal representation of users’ digest use preferences.
2+
//
3+
// Something like this _might_ be eventually made available as a public API:
4+
// before doing so, carefully think whether the API should be modified before we commit to it.
5+
6+
package digests
7+
8+
import (
9+
"errors"
10+
"fmt"
11+
12+
"github.com/opencontainers/go-digest"
13+
)
14+
15+
// Options records users’ preferences for used digest algorithm usage.
16+
// It is a value type and can be copied using ordinary assignment.
17+
//
18+
// It can only be created using one of the provided constructors.
19+
type Options struct {
20+
initialized bool // To prevent uses that don’t call a public constructor; this is necessary to enforce the .Available() promise.
21+
22+
// If any of the fields below is set, it is guarateed to be .Available().
23+
24+
mustUse digest.Algorithm // If not "", written digests must use this algorithm.
25+
prefer digest.Algorithm // If not "", use this algorithm whenever possible.
26+
defaultAlgo digest.Algorithm // If not "", use this algorithm if there is no reason to use anything else.
27+
}
28+
29+
// CanonicalDefault is Options which default to using digest.Canonical if there is no reason to use a different algorithm
30+
// (e.g. when there is no pre-existing digest).
31+
//
32+
// The configuration can be customized using .WithPreferred() or .WithDefault().
33+
func CanonicalDefault() Options {
34+
// This does not set .defaultAlgo so that .WithDefault() can be called (once).
35+
return Options{
36+
initialized: true,
37+
}
38+
}
39+
40+
// MustUse constructs Options which always use algo.
41+
func MustUse(algo digest.Algorithm) (Options, error) {
42+
// We don’t provide Options.WithMustUse because there is no other option that makes a difference
43+
// once .mustUse is set.
44+
if !algo.Available() {
45+
return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String())
46+
}
47+
return Options{
48+
initialized: true,
49+
mustUse: algo,
50+
}, nil
51+
}
52+
53+
// WithPreferred returns a copy of o with a “preferred” algorithm set to algo.
54+
// The preferred algorithm is used whenever possible (but if there is a strict requirement to use something else, it will be overridden).
55+
func (o Options) WithPreferred(algo digest.Algorithm) (Options, error) {
56+
if err := o.ensureInitialized(); err != nil {
57+
return Options{}, err
58+
}
59+
if o.prefer != "" {
60+
return Options{}, errors.New("digests.Options already have a 'prefer' algorithm configured")
61+
}
62+
63+
if !algo.Available() {
64+
return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String())
65+
}
66+
o.prefer = algo
67+
return o, nil
68+
}
69+
70+
// WithDefault returns a copy of o with a “default” algorithm set to algo.
71+
// The default algorithm is used if there is no reason to use anything else (e.g. when there is no pre-existing digest).
72+
func (o Options) WithDefault(algo digest.Algorithm) (Options, error) {
73+
if err := o.ensureInitialized(); err != nil {
74+
return Options{}, err
75+
}
76+
if o.defaultAlgo != "" {
77+
return Options{}, errors.New("digests.Options already have a 'default' algorithm configured")
78+
}
79+
80+
if !algo.Available() {
81+
return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String())
82+
}
83+
o.defaultAlgo = algo
84+
return o, nil
85+
}
86+
87+
// ensureInitialized returns an error if o is not initialized.
88+
func (o Options) ensureInitialized() error {
89+
if !o.initialized {
90+
return errors.New("internal error: use of uninitialized digests.Options")
91+
}
92+
return nil
93+
}
94+
95+
// Situation records the context in which a digest is being chosen.
96+
type Situation struct {
97+
Preexisting digest.Digest // If not "", a pre-existing digest value (frequently one which is cheaper to use than others)
98+
CannotChangeAlgorithmReason string // The reason why we must use Preexisting, or "" if we can use other algorithms.
99+
}
100+
101+
// Choose chooses a digest algorithm based on the options and the situation.
102+
func (o Options) Choose(s Situation) (digest.Algorithm, error) {
103+
if err := o.ensureInitialized(); err != nil {
104+
return "", err
105+
}
106+
107+
if s.CannotChangeAlgorithmReason != "" && s.Preexisting == "" {
108+
return "", fmt.Errorf("internal error: digests.Situation.CannotChangeAlgorithmReason is set but Preexisting is empty")
109+
}
110+
111+
var choice digest.Algorithm // = what we want to use
112+
switch {
113+
case o.mustUse != "":
114+
choice = o.mustUse
115+
case s.CannotChangeAlgorithmReason != "":
116+
choice = s.Preexisting.Algorithm()
117+
if !choice.Available() {
118+
return "", fmt.Errorf("existing digest uses unimplemented algorithm %s", choice)
119+
}
120+
case o.prefer != "":
121+
choice = o.prefer
122+
case s.Preexisting != "" && s.Preexisting.Algorithm().Available():
123+
choice = s.Preexisting.Algorithm()
124+
case o.defaultAlgo != "":
125+
choice = o.defaultAlgo
126+
default:
127+
choice = digest.Canonical // We assume digest.Canonical is always available.
128+
}
129+
130+
if s.CannotChangeAlgorithmReason != "" && choice != s.Preexisting.Algorithm() {
131+
return "", fmt.Errorf("requested to always use digest algorithm %s but we cannot replace existing digest algorithm %s: %s",
132+
choice, s.Preexisting.Algorithm(), s.CannotChangeAlgorithmReason)
133+
}
134+
135+
return choice, nil
136+
}
137+
138+
// MustUseSet returns an algorithm if o is set to always use a specific algorithm, "" if it is flexible.
139+
func (o Options) MustUseSet() digest.Algorithm {
140+
// We don’t do .ensureInitialized() because that would require an extra error value just for that.
141+
// This should not be a part of any public API either way.
142+
if o.mustUse != "" {
143+
return o.mustUse
144+
}
145+
return ""
146+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package digests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/opencontainers/go-digest"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestCanonicalDefault(t *testing.T) {
12+
o := CanonicalDefault()
13+
assert.Equal(t, Options{initialized: true}, o)
14+
}
15+
16+
func TestMustUse(t *testing.T) {
17+
o, err := MustUse(digest.SHA512)
18+
require.NoError(t, err)
19+
assert.Equal(t, Options{
20+
initialized: true,
21+
mustUse: digest.SHA512,
22+
}, o)
23+
24+
_, err = MustUse(digest.Algorithm("this is not a known algorithm"))
25+
require.Error(t, err)
26+
}
27+
28+
func TestOptionsWithPreferred(t *testing.T) {
29+
preferSHA512, err := CanonicalDefault().WithPreferred(digest.SHA512)
30+
require.NoError(t, err)
31+
assert.Equal(t, Options{
32+
initialized: true,
33+
prefer: digest.SHA512,
34+
}, preferSHA512)
35+
36+
for _, c := range []struct {
37+
base Options
38+
algo digest.Algorithm
39+
}{
40+
{ // Uninitialized Options
41+
base: Options{},
42+
algo: digest.SHA256,
43+
},
44+
{ // Unavailable algorithm
45+
base: CanonicalDefault(),
46+
algo: digest.Algorithm("this is not a known algorithm"),
47+
},
48+
{ // WithPreferred already called
49+
base: preferSHA512,
50+
algo: digest.SHA512,
51+
},
52+
} {
53+
_, err := c.base.WithPreferred(c.algo)
54+
assert.Error(t, err)
55+
}
56+
}
57+
58+
func TestOptionsWithDefault(t *testing.T) {
59+
defaultSHA512, err := CanonicalDefault().WithDefault(digest.SHA512)
60+
require.NoError(t, err)
61+
assert.Equal(t, Options{
62+
initialized: true,
63+
defaultAlgo: digest.SHA512,
64+
}, defaultSHA512)
65+
66+
for _, c := range []struct {
67+
base Options
68+
algo digest.Algorithm
69+
}{
70+
{ // Uninitialized Options
71+
base: Options{},
72+
algo: digest.SHA256,
73+
},
74+
{ // Unavailable algorithm
75+
base: CanonicalDefault(),
76+
algo: digest.Algorithm("this is not a known algorithm"),
77+
},
78+
{ // WithDefault already called
79+
base: defaultSHA512,
80+
algo: digest.SHA512,
81+
},
82+
} {
83+
_, err := c.base.WithDefault(c.algo)
84+
assert.Error(t, err)
85+
}
86+
}
87+
88+
func TestOptionsChoose(t *testing.T) {
89+
sha512Digest := digest.Digest("sha512:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")
90+
unknownDigest := digest.Digest("unknown:abcd1234")
91+
92+
// The tests use sha512 = pre-existing if any; sha384 = primary configured; sha256 = supposedly irrelevant.
93+
94+
mustSHA384, err := MustUse(digest.SHA384)
95+
require.NoError(t, err)
96+
mustSHA384, err = mustSHA384.WithPreferred(digest.SHA256)
97+
require.NoError(t, err)
98+
mustSHA384, err = mustSHA384.WithDefault(digest.SHA256)
99+
require.NoError(t, err)
100+
101+
preferSHA384, err := CanonicalDefault().WithPreferred(digest.SHA384)
102+
require.NoError(t, err)
103+
preferSHA384, err = preferSHA384.WithDefault(digest.SHA256)
104+
require.NoError(t, err)
105+
106+
defaultSHA384, err := CanonicalDefault().WithDefault(digest.SHA384)
107+
require.NoError(t, err)
108+
109+
cases := []struct {
110+
opts Options
111+
wantDefault digest.Algorithm
112+
wantPreexisting digest.Algorithm // Pre-existing sha512
113+
wantCannotChange digest.Algorithm // Pre-existing sha512, CannotChange
114+
wantUnavailable digest.Algorithm // Pre-existing unavailable
115+
}{
116+
{
117+
opts: Options{}, // uninitialized
118+
wantDefault: "",
119+
wantPreexisting: "",
120+
wantCannotChange: "",
121+
wantUnavailable: "",
122+
},
123+
{
124+
opts: mustSHA384,
125+
wantDefault: digest.SHA384,
126+
wantPreexisting: digest.SHA384,
127+
wantCannotChange: "",
128+
// Warning: We don’t generally _promise_ that unavailable digests are going to be silently ignored
129+
// in these situations (e.g. we might still try to validate them when reading inputs).
130+
wantUnavailable: digest.SHA384,
131+
},
132+
{
133+
opts: preferSHA384,
134+
wantDefault: digest.SHA384,
135+
wantPreexisting: digest.SHA384,
136+
wantCannotChange: digest.SHA512,
137+
wantUnavailable: digest.SHA384,
138+
},
139+
{
140+
opts: defaultSHA384,
141+
wantDefault: digest.SHA384,
142+
wantPreexisting: digest.SHA512,
143+
wantCannotChange: digest.SHA512,
144+
wantUnavailable: digest.SHA384,
145+
},
146+
{
147+
opts: CanonicalDefault(),
148+
wantDefault: digest.SHA256,
149+
wantPreexisting: digest.SHA512,
150+
wantCannotChange: digest.SHA512,
151+
wantUnavailable: digest.SHA256,
152+
},
153+
}
154+
for _, c := range cases {
155+
run := func(s Situation, want digest.Algorithm) {
156+
res, err := c.opts.Choose(s)
157+
if want == "" {
158+
require.Error(t, err)
159+
} else {
160+
require.NoError(t, err)
161+
assert.Equal(t, want, res)
162+
}
163+
}
164+
165+
run(Situation{}, c.wantDefault)
166+
run(Situation{Preexisting: sha512Digest}, c.wantPreexisting)
167+
run(Situation{Preexisting: sha512Digest, CannotChangeAlgorithmReason: "test reason"}, c.wantCannotChange)
168+
run(Situation{Preexisting: unknownDigest}, c.wantUnavailable)
169+
170+
run(Situation{Preexisting: unknownDigest, CannotChangeAlgorithmReason: "test reason"}, "")
171+
run(Situation{CannotChangeAlgorithmReason: "test reason"}, "") // CannotChangeAlgorithm with missing Preexisting
172+
}
173+
}
174+
175+
func TestOptionsMustUseSet(t *testing.T) {
176+
mustSHA512, err := MustUse(digest.SHA512)
177+
require.NoError(t, err)
178+
prefersSHA512, err := CanonicalDefault().WithPreferred(digest.SHA512)
179+
require.NoError(t, err)
180+
defaultSHA512, err := CanonicalDefault().WithDefault(digest.SHA512)
181+
require.NoError(t, err)
182+
for _, c := range []struct {
183+
opts Options
184+
want digest.Algorithm
185+
}{
186+
{
187+
opts: mustSHA512,
188+
want: digest.SHA512,
189+
},
190+
{
191+
opts: prefersSHA512,
192+
want: "",
193+
},
194+
{
195+
opts: defaultSHA512,
196+
want: "",
197+
},
198+
} {
199+
assert.Equal(t, c.want, c.opts.MustUseSet())
200+
}
201+
}

0 commit comments

Comments
 (0)