Skip to content

Commit da908ae

Browse files
authored
Merge pull request #110 from infosiftr/parallelism-redux
(re-)Implement parallelism in deploy
2 parents bf729c6 + dfb595a commit da908ae

File tree

8 files changed

+778
-72
lines changed

8 files changed

+778
-72
lines changed

.test/deploy-dry-run-test.json

Lines changed: 385 additions & 0 deletions
Large diffs are not rendered by default.

.test/test.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,15 @@ if [ -n "$doDeploy" ]; then
250250
empty
251251
')" # stored in a variable for easier debugging ("bash -x")
252252

253-
time "$coverage/bin/deploy" <<<"$json"
253+
time "$coverage/bin/deploy" --dry-run --parallel <<<"$json" > "$dir/deploy-dry-run-test.json"
254+
# port is random, so let's de-randomize it:
255+
sed -i -e "s/localhost:$registryPort/localhost:3000/g" "$dir/deploy-dry-run-test.json"
256+
257+
time "$coverage/bin/deploy" --parallel <<<"$json"
258+
259+
# now that we're done with deploying, a second dry-run should come back empty (this time without parallel to test other codepaths)
260+
time empty="$("$coverage/bin/deploy" --dry-run <<<"$json")"
261+
( set -x; test -z "$empty" )
254262

255263
docker rm -vf meta-scripts-test-registry
256264
trap - EXIT

Jenkinsfile.deploy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ node('put-shared') { ansiColor('xterm') {
101101
./.go-env.sh go build -trimpath -o bin/deploy ./cmd/deploy
102102
fi
103103
)
104-
.scripts/bin/deploy < filtered-deploy.json
104+
.scripts/bin/deploy --parallel < filtered-deploy.json
105105
'''
106106
}
107107
}

cmd/deploy/input.go

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"maps"
89

910
"github.com/docker-library/meta-scripts/registry"
1011

@@ -43,10 +44,17 @@ type inputNormalized struct {
4344
Lookup map[ociregistry.Digest]registry.Reference `json:"lookup,omitempty"`
4445

4546
// Data and CopyFrom are mutually exclusive
46-
Data []byte `json:"data"`
47-
CopyFrom *registry.Reference `json:"copyFrom"`
47+
Data []byte `json:"data,omitempty"`
48+
CopyFrom *registry.Reference `json:"copyFrom,omitempty"`
4849

49-
Do func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) `json:"-"`
50+
// if CopyFrom is nil and Type is manifest, this will be set (used by "do")
51+
MediaType string `json:"mediaType,omitempty"`
52+
}
53+
54+
func (normal inputNormalized) clone() inputNormalized {
55+
// normal.Lookup is the only thing we have concurrency issues with, so it's the only thing we'll explicitly clone 😇
56+
normal.Lookup = maps.Clone(normal.Lookup)
57+
return normal
5058
}
5159

5260
func normalizeInputRefs(deployType deployType, rawRefs []string) ([]registry.Reference, ociregistry.Digest, error) {
@@ -215,13 +223,23 @@ func NormalizeInput(raw inputRaw) (inputNormalized, error) {
215223
normal.Refs[i].Digest = refsDigest
216224
}
217225

226+
// if we have a digest and we're performing a copy, the tag we're copying *from* is no longer relevant information
227+
if refsDigest != "" && normal.CopyFrom != nil {
228+
normal.CopyFrom.Tag = ""
229+
}
230+
218231
// explicitly clear tag and digest from lookup entries (now that we've inferred any "CopyFrom" out of them, they no longer have any meaning)
219232
for d, ref := range normal.Lookup {
233+
if d == "" && refsDigest == "" && ref.Tag != "" && normal.CopyFrom != nil && ref.Tag == normal.CopyFrom.Tag {
234+
// let the "fallback" ref keep a tag when it's the tag we're copying and there's no known digest (this allows our normalized objects to still be completely valid "raw" inputs)
235+
continue
236+
}
220237
ref.Tag = ""
221238
ref.Digest = ""
222239
normal.Lookup[d] = ref
223240
}
224241

242+
// front-load some validation / data extraction for "normal.do" to work
225243
switch normal.Type {
226244
case typeManifest:
227245
if normal.CopyFrom == nil {
@@ -240,33 +258,98 @@ func NormalizeInput(raw inputRaw) (inputNormalized, error) {
240258
// and our logic for pushing children needs to know the mediaType (see the GHSAs referenced above)
241259
return normal, fmt.Errorf("%s: pushing manifest but missing 'mediaType'", debugId)
242260
}
243-
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
244-
return registry.EnsureManifest(ctx, dstRef, normal.Data, mediaTypeHaver.MediaType, normal.Lookup)
245-
}
261+
normal.MediaType = mediaTypeHaver.MediaType
262+
}
263+
264+
case typeBlob:
265+
if normal.CopyFrom != nil && normal.CopyFrom.Digest == "" {
266+
return normal, fmt.Errorf("%s: blobs are always by-digest, and thus need a digest: %s", debugId, normal.CopyFrom)
267+
}
268+
269+
default:
270+
panic("unknown type: " + string(normal.Type))
271+
// panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error)
272+
}
273+
274+
return normal, nil
275+
}
276+
277+
// WARNING: many of these codepaths will end up writing to "normal.Lookup", which because it's a map is passed by reference, so this method is *not* safe for concurrent invocation on a single "normal" object! see "normal.clone" (above)
278+
func (normal inputNormalized) do(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
279+
switch normal.Type {
280+
case typeManifest:
281+
if normal.CopyFrom == nil {
282+
// TODO panic on bad data, like MediaType being empty?
283+
return registry.EnsureManifest(ctx, dstRef, normal.Data, normal.MediaType, normal.Lookup)
246284
} else {
247-
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
248-
return registry.CopyManifest(ctx, *normal.CopyFrom, dstRef, normal.Lookup)
249-
}
285+
return registry.CopyManifest(ctx, *normal.CopyFrom, dstRef, normal.Lookup)
250286
}
251287

252288
case typeBlob:
253289
if normal.CopyFrom == nil {
254-
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
255-
return registry.EnsureBlob(ctx, dstRef, int64(len(normal.Data)), bytes.NewReader(normal.Data))
256-
}
290+
return registry.EnsureBlob(ctx, dstRef, int64(len(normal.Data)), bytes.NewReader(normal.Data))
257291
} else {
258-
if normal.CopyFrom.Digest == "" {
259-
return normal, fmt.Errorf("%s: blobs are always by-digest, and thus need a digest: %s", debugId, normal.CopyFrom)
292+
return registry.CopyBlob(ctx, *normal.CopyFrom, dstRef)
293+
}
294+
295+
default:
296+
panic("unknown type: " + string(normal.Type))
297+
// panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error)
298+
}
299+
}
300+
301+
// "do", but doesn't mutate state at all (just tells us whether "do" would've done anything)
302+
func (normal inputNormalized) dryRun(ctx context.Context, dstRef registry.Reference) (bool, error) {
303+
targetDigest := dstRef.Digest
304+
var lookupType registry.LookupType
305+
switch normal.Type {
306+
case typeManifest:
307+
lookupType = registry.LookupTypeManifest
308+
if targetDigest == "" {
309+
// if we don't have a digest here, it must be because we're copying from tag to tag, so we'll just assume normal.CopyFrom is non-nil and let the runtime panic for us if the normalization above doesn't have our back
310+
r, err := registry.Lookup(ctx, *normal.CopyFrom, &registry.LookupOptions{
311+
Type: lookupType,
312+
Head: true,
313+
})
314+
if err != nil {
315+
return true, err
316+
}
317+
if r == nil {
318+
return true, fmt.Errorf("%s: manifest-to-copy (%s) is 404", dstRef.String(), normal.CopyFrom.String())
319+
}
320+
targetDigest = r.Descriptor().Digest
321+
r.Close()
322+
if targetDigest == "" {
323+
return true, fmt.Errorf("%s: manifest-to-copy (%s) is missing digest!", dstRef.String(), normal.CopyFrom.String())
260324
}
261-
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
262-
return registry.CopyBlob(ctx, *normal.CopyFrom, dstRef)
325+
if dstRef.Tag == "" {
326+
// if we don't have an explicit destination tag, this is considered a request to copy-manifest-from-tag-but-push-by-digest, which is weird, but valid, so we need to copy up that digest into what we look for on the destination side
327+
dstRef.Digest = targetDigest
263328
}
264329
}
265-
330+
case typeBlob:
331+
lookupType = registry.LookupTypeBlob
332+
if targetDigest == "" {
333+
// see validation above in normalization
334+
panic("blob ref missing digest, this should never happen: " + dstRef.String())
335+
}
266336
default:
267337
panic("unknown type: " + string(normal.Type))
268338
// panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error)
269339
}
270340

271-
return normal, nil
341+
r, err := registry.Lookup(ctx, dstRef, &registry.LookupOptions{
342+
Type: lookupType,
343+
Head: true,
344+
})
345+
if err != nil {
346+
return true, err
347+
}
348+
if r == nil {
349+
// 404!
350+
return true, nil
351+
}
352+
dstDigest := r.Descriptor().Digest
353+
r.Close()
354+
return targetDigest != dstDigest, nil
272355
}

0 commit comments

Comments
 (0)