Skip to content

Commit 75c2853

Browse files
committed
Implement parallelism in deploy
This requires the input to be in the correct order (children first), but that was already an implied constraint. In my testing, this takes our `.test/test.sh --deploy` block testing deploy from ~5s down to ~2.5s. I used "show timestamps" in our GHA CI jobs to compare them also, and they're currently at ~7s, and after this change they drop to ~1s. I've tested this with my personal deploy job, which was taking ~2m40s before, and is ~1m18s after.
1 parent 0e4a5d2 commit 75c2853

File tree

6 files changed

+232
-49
lines changed

6 files changed

+232
-49
lines changed

.test/test.sh

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,43 @@ if [ -n "$doDeploy" ]; then
168168
data: ("json string" | @json + "\n" | @base64),
169169
},
170170
171+
# test pushing a full, actual image (tianon/true:oci@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d), all parts
172+
{
173+
# config blob
174+
type: "blob",
175+
refs: [$reg+"/true"],
176+
data: "ewoJImFyY2hpdGVjdHVyZSI6ICJhbWQ2NCIsCgkiY29uZmlnIjogewoJCSJDbWQiOiBbCgkJCSIvdHJ1ZSIKCQldCgl9LAoJImNyZWF0ZWQiOiAiMjAyMy0wMi0wMVQwNjo1MToxMVoiLAoJImhpc3RvcnkiOiBbCgkJewoJCQkiY3JlYXRlZCI6ICIyMDIzLTAyLTAxVDA2OjUxOjExWiIsCgkJCSJjcmVhdGVkX2J5IjogImh0dHBzOi8vZ2l0aHViLmNvbS90aWFub24vZG9ja2VyZmlsZXMvdHJlZS9tYXN0ZXIvdHJ1ZSIKCQl9CgldLAoJIm9zIjogImxpbnV4IiwKCSJyb290ZnMiOiB7CgkJImRpZmZfaWRzIjogWwoJCQkic2hhMjU2OjY1YjVhNDU5M2NjNjFkM2VhNmQzNTVmYjk3YzA0MzBkODIwZWUyMWFhODUzNWY1ZGU0NWU3NWMzMTk1NGI3NDMiCgkJXSwKCQkidHlwZSI6ICJsYXllcnMiCgl9Cn0K",
177+
},
178+
{
179+
# layer blob
180+
type: "blob",
181+
refs: [$reg+"/true"],
182+
data: "H4sIAAAAAAACAyspKk1loDEwAAJTU1MwDQTotIGhuQmcDRE3MzM0YlAwYKADKC0uSSxSUGAYoaDe1ceNiZERzmdisGMA8SoYHMB8Byx6HBgsGGA6QDQrmiwyXQPl1cDlIUG9wYaflWEUDDgAAIAGdJIABAAA",
183+
},
184+
{
185+
type: "manifest",
186+
refs: [ "oci", "latest", (range(0; 10)) | $reg+"/true:\(.)", $reg+"/foo/true:\(.)" ], # test pushing a whole bunch of tags in multiple repos
187+
lookup: {
188+
# a few explicit lookup entries for better code coverage (dep calculation during parallelization)
189+
"sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": ($reg+"/true"),
190+
"": ($reg+"/true"),
191+
},
192+
data: {
193+
schemaVersion: 2,
194+
mediaType: "application/vnd.oci.image.manifest.v1+json",
195+
config: {
196+
mediaType: "application/vnd.oci.image.config.v1+json",
197+
digest: "sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e",
198+
size: 396,
199+
},
200+
layers: [ {
201+
mediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
202+
digest: "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6",
203+
size: 117,
204+
} ],
205+
},
206+
},
207+
171208
# test blob mounting between repositories
172209
{
173210
type: "blob",
@@ -213,7 +250,7 @@ if [ -n "$doDeploy" ]; then
213250
empty
214251
')" # stored in a variable for easier debugging ("bash -x")
215252

216-
"$coverage/bin/deploy" <<<"$json"
253+
time "$coverage/bin/deploy" <<<"$json"
217254

218255
docker rm -vf meta-scripts-test-registry
219256
trap - EXIT

cmd/deploy/input.go

Lines changed: 36 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

@@ -46,7 +47,14 @@ type inputNormalized struct {
4647
Data []byte `json:"data"`
4748
CopyFrom *registry.Reference `json:"copyFrom"`
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) {
@@ -222,6 +230,7 @@ func NormalizeInput(raw inputRaw) (inputNormalized, error) {
222230
normal.Lookup[d] = ref
223231
}
224232

233+
// front-load some validation / data extraction for "normal.do" to work
225234
switch normal.Type {
226235
case typeManifest:
227236
if normal.CopyFrom == nil {
@@ -240,33 +249,42 @@ func NormalizeInput(raw inputRaw) (inputNormalized, error) {
240249
// and our logic for pushing children needs to know the mediaType (see the GHSAs referenced above)
241250
return normal, fmt.Errorf("%s: pushing manifest but missing 'mediaType'", debugId)
242251
}
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-
}
252+
normal.MediaType = mediaTypeHaver.MediaType
253+
}
254+
255+
case typeBlob:
256+
if normal.CopyFrom != nil && normal.CopyFrom.Digest == "" {
257+
return normal, fmt.Errorf("%s: blobs are always by-digest, and thus need a digest: %s", debugId, normal.CopyFrom)
258+
}
259+
260+
default:
261+
panic("unknown type: " + string(normal.Type))
262+
// panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error)
263+
}
264+
265+
return normal, nil
266+
}
267+
268+
// 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)
269+
func (normal inputNormalized) do(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
270+
switch normal.Type {
271+
case typeManifest:
272+
if normal.CopyFrom == nil {
273+
// TODO panic on bad data, like MediaType being empty?
274+
return registry.EnsureManifest(ctx, dstRef, normal.Data, normal.MediaType, normal.Lookup)
246275
} 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-
}
276+
return registry.CopyManifest(ctx, *normal.CopyFrom, dstRef, normal.Lookup)
250277
}
251278

252279
case typeBlob:
253280
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-
}
281+
return registry.EnsureBlob(ctx, dstRef, int64(len(normal.Data)), bytes.NewReader(normal.Data))
257282
} 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)
260-
}
261-
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
262-
return registry.CopyBlob(ctx, *normal.CopyFrom, dstRef)
263-
}
283+
return registry.CopyBlob(ctx, *normal.CopyFrom, dstRef)
264284
}
265285

266286
default:
267287
panic("unknown type: " + string(normal.Type))
268288
// panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error)
269289
}
270-
271-
return normal, nil
272290
}

cmd/deploy/input_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ func TestNormalizeInput(t *testing.T) {
281281
"refs": [ "localhost:5000/example:test" ],
282282
"data": {"mediaType": "application/vnd.oci.image.index.v1+json"}
283283
}`,
284-
`{"type":"manifest","refs":["localhost:5000/example:test@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null}`,
284+
`{"type":"manifest","refs":["localhost:5000/example:test@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null,"mediaType":"application/vnd.oci.image.index.v1+json"}`,
285285
},
286286
{
287287
"manifest raw",
@@ -290,7 +290,7 @@ func TestNormalizeInput(t *testing.T) {
290290
"refs": [ "localhost:5000/example" ],
291291
"data": "eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0="
292292
}`,
293-
`{"type":"manifest","refs":["localhost:5000/example@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null}`,
293+
`{"type":"manifest","refs":["localhost:5000/example@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null,"mediaType":"application/vnd.oci.image.index.v1+json"}`,
294294
},
295295

296296
{
@@ -301,7 +301,7 @@ func TestNormalizeInput(t *testing.T) {
301301
"lookup": { "sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d": "tianon/true" },
302302
"data": {"mediaType": "application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","size":1165}],"schemaVersion":2}
303303
}`,
304-
`{"type":"manifest","refs":["localhost:5000/example:test@sha256:0cb474919526d040392883b84e5babb65a149cc605b89b117781ab94e88a5e86"],"lookup":{"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIiwibWFuaWZlc3RzIjpbeyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQub2NpLmltYWdlLm1hbmlmZXN0LnYxK2pzb24iLCJkaWdlc3QiOiJzaGEyNTY6OWVmNDJmMWQ2MDJmYjQyM2ZhZDkzNWFhYzFjYWEwY2ZkYmNlMWFkN2VkY2U2NGQwODBhNGViN2IxM2Y3Y2Q5ZCIsInNpemUiOjExNjV9XSwic2NoZW1hVmVyc2lvbiI6Mn0=","copyFrom":null}`,
304+
`{"type":"manifest","refs":["localhost:5000/example:test@sha256:0cb474919526d040392883b84e5babb65a149cc605b89b117781ab94e88a5e86"],"lookup":{"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIiwibWFuaWZlc3RzIjpbeyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQub2NpLmltYWdlLm1hbmlmZXN0LnYxK2pzb24iLCJkaWdlc3QiOiJzaGEyNTY6OWVmNDJmMWQ2MDJmYjQyM2ZhZDkzNWFhYzFjYWEwY2ZkYmNlMWFkN2VkY2U2NGQwODBhNGViN2IxM2Y3Y2Q5ZCIsInNpemUiOjExNjV9XSwic2NoZW1hVmVyc2lvbiI6Mn0=","copyFrom":null,"mediaType":"application/vnd.oci.image.index.v1+json"}`,
305305
},
306306
{
307307
"image",
@@ -311,7 +311,7 @@ func TestNormalizeInput(t *testing.T) {
311311
"lookup": { "": "tianon/true" },
312312
"data": {"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1471,"digest":"sha256:690912094c0165c489f874c72cee4ba208c28992c0699fa6e10d8cc59f93fec9"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":129,"digest":"sha256:4c74d744397d4bcbd3079d9c82a87b80d43da376313772978134d1288f20518c"}]}
313313
}`,
314-
`{"type":"manifest","refs":["localhost:5000/example@sha256:1c70f9d471b83100c45d5a218d45bbf7e073e11ea5043758a020379a7c78f878"],"lookup":{"":"tianon/true"},"data":"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNDcxLCJkaWdlc3QiOiJzaGEyNTY6NjkwOTEyMDk0YzAxNjVjNDg5Zjg3NGM3MmNlZTRiYTIwOGMyODk5MmMwNjk5ZmE2ZTEwZDhjYzU5ZjkzZmVjOSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMjksImRpZ2VzdCI6InNoYTI1Njo0Yzc0ZDc0NDM5N2Q0YmNiZDMwNzlkOWM4MmE4N2I4MGQ0M2RhMzc2MzEzNzcyOTc4MTM0ZDEyODhmMjA1MThjIn1dfQ==","copyFrom":null}`,
314+
`{"type":"manifest","refs":["localhost:5000/example@sha256:1c70f9d471b83100c45d5a218d45bbf7e073e11ea5043758a020379a7c78f878"],"lookup":{"":"tianon/true"},"data":"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNDcxLCJkaWdlc3QiOiJzaGEyNTY6NjkwOTEyMDk0YzAxNjVjNDg5Zjg3NGM3MmNlZTRiYTIwOGMyODk5MmMwNjk5ZmE2ZTEwZDhjYzU5ZjkzZmVjOSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMjksImRpZ2VzdCI6InNoYTI1Njo0Yzc0ZDc0NDM5N2Q0YmNiZDMwNzlkOWM4MmE4N2I4MGQ0M2RhMzc2MzEzNzcyOTc4MTM0ZDEyODhmMjA1MThjIn1dfQ==","copyFrom":null,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}`,
315315
},
316316

317317
{

cmd/deploy/main.go

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import (
77
"os"
88
"os/exec"
99
"os/signal"
10+
"sync"
11+
12+
"github.com/docker-library/meta-scripts/registry"
13+
14+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1015
)
1116

1217
func main() {
@@ -32,6 +37,11 @@ func main() {
3237
panic(err)
3338
}
3439

40+
// a set of RWMutex objects for synchronizing the pushing of "child" objects before their parents later in the list of documents
41+
// for every RWMutex, it will be *write*-locked during push, and *read*-locked during reading (which means we won't limit the parallelization of multiple parents after a given child is pushed, but we will stop parents from being pushed before their children)
42+
childMutexes := sync.Map{}
43+
wg := sync.WaitGroup{}
44+
3545
dec := json.NewDecoder(stdout)
3646
for dec.More() {
3747
var raw inputRaw
@@ -48,26 +58,128 @@ func main() {
4858
}
4959
refsDigest := normal.Refs[0].Digest
5060

51-
if normal.CopyFrom == nil {
52-
fmt.Printf("Pushing %s %s:\n", raw.Type, refsDigest)
61+
var logSuffix string = " (" + string(raw.Type) + ") "
62+
if normal.CopyFrom != nil {
63+
// normal copy (one repo/registry to another)
64+
logSuffix = " 🤝" + logSuffix + normal.CopyFrom.String()
65+
// "localhost:32774/test 🤝 (manifest) tianon/test@sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f"
5366
} else {
54-
fmt.Printf("Copying %s %s:\n", raw.Type, *normal.CopyFrom)
67+
// push (raw/embedded blob or manifest data)
68+
logSuffix = " 🦾" + logSuffix + string(refsDigest)
69+
// "localhost:32774/test 🦾 (blob) sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1"
5570
}
71+
startedPrefix := "❔ "
72+
successPrefix := "✅ "
73+
failurePrefix := "❌ "
74+
75+
// locks are per-digest, but refs might be 20 tags on the same digest, so we need to get one write lock per repo@digest and release it when the first tag completes, and every other tag needs a read lock
76+
seenRefs := map[string]bool{}
5677

5778
for _, ref := range normal.Refs {
58-
fmt.Printf(" - %s", ref.StringWithKnownDigest(refsDigest))
59-
desc, err := normal.Do(ctx, ref)
60-
if err != nil {
61-
fmt.Fprintf(os.Stderr, " -- ERROR: %v\n", err)
62-
os.Exit(1)
63-
return
64-
}
65-
if ref.Digest == "" && refsDigest == "" {
66-
fmt.Printf("@%s", desc.Digest)
79+
ref := ref // https://github.com/golang/go/issues/60078
80+
81+
necessaryReadLockRefs := []registry.Reference{}
82+
83+
// before parallelization, collect the pushing "child" mutex we need to lock for writing right away (but only for the first entry)
84+
var mutex *sync.RWMutex
85+
if ref.Digest != "" {
86+
lockRef := ref
87+
lockRef.Tag = ""
88+
lockRefStr := lockRef.String()
89+
if seenRefs[lockRefStr] {
90+
// if we've already seen this specific ref for this input, we need a read lock, not a write lock (since they're per-repo@digest)
91+
necessaryReadLockRefs = append(necessaryReadLockRefs, lockRef)
92+
} else {
93+
seenRefs[lockRefStr] = true
94+
lock, _ := childMutexes.LoadOrStore(lockRefStr, &sync.RWMutex{})
95+
mutex = lock.(*sync.RWMutex)
96+
// if we have a "child" mutex, lock it immediately so we don't create a race between inputs
97+
mutex.Lock() // (this gets unlocked in the goroutine below)
98+
// this is sane to lock here because interdependent inputs are required to be in-order (children first), so if this hangs it's 100% a bug in the input order
99+
}
67100
}
68-
fmt.Println()
69-
}
70101

71-
fmt.Println()
102+
// make a (deep) copy of "normal" so that we can use it in a goroutine ("normal.do" is not safe for concurrent invocation)
103+
normal := normal.clone()
104+
105+
wg.Add(1)
106+
go func() {
107+
defer wg.Done()
108+
109+
if mutex != nil {
110+
defer mutex.Unlock()
111+
}
112+
113+
// before we start this job (parallelized), if it's a raw data job we need to parse the raw data and see if any of the "children" are objects we're still in the process of pushing (from a previously parallel job)
114+
if len(normal.Data) > 2 { // needs to at least be bigger than "{}" for us to care (anything else either doesn't have data or can't have children)
115+
// explicitly ignoring errors because this might not actually be JSON (or even a manifest at all!); this is best-effort
116+
// TODO optimize this by checking whether normal.Data matches "^\s*{.+}\s*$" first so we have some assurance it might work before we go further?
117+
manifestChildren, _ := registry.ParseManifestChildren(normal.Data)
118+
childDescs := []ocispec.Descriptor{}
119+
childDescs = append(childDescs, manifestChildren.Manifests...)
120+
if manifestChildren.Config != nil {
121+
childDescs = append(childDescs, *manifestChildren.Config)
122+
}
123+
childDescs = append(childDescs, manifestChildren.Layers...)
124+
for _, childDesc := range childDescs {
125+
childRef := ref
126+
childRef.Digest = childDesc.Digest
127+
necessaryReadLockRefs = append(necessaryReadLockRefs, childRef)
128+
129+
// these read locks are cheap, so let's be aggressive with our "lookup" refs too
130+
if lookupRef, ok := normal.Lookup[childDesc.Digest]; ok {
131+
lookupRef.Digest = childDesc.Digest
132+
necessaryReadLockRefs = append(necessaryReadLockRefs, lookupRef)
133+
}
134+
if fallbackRef, ok := normal.Lookup[""]; ok {
135+
fallbackRef.Digest = childDesc.Digest
136+
necessaryReadLockRefs = append(necessaryReadLockRefs, fallbackRef)
137+
}
138+
}
139+
}
140+
// we don't *know* that all the lookup references are children, but if any of them have an explicit digest, let's treat them as potential children too (which is fair, because they *are* explicit potential references that it's sane to make sure exist)
141+
for digest, lookupRef := range normal.Lookup {
142+
necessaryReadLockRefs = append(necessaryReadLockRefs, lookupRef)
143+
if digest != lookupRef.Digest {
144+
lookupRef.Digest = digest
145+
necessaryReadLockRefs = append(necessaryReadLockRefs, lookupRef)
146+
}
147+
}
148+
// if we're going to do a copy, we need to *also* include the artifact we're copying in our list
149+
if normal.CopyFrom != nil {
150+
necessaryReadLockRefs = append(necessaryReadLockRefs, *normal.CopyFrom)
151+
}
152+
// ok, we've built up a list, let's start grabbing (ro) mutexes
153+
seenChildren := map[string]bool{}
154+
for _, lockRef := range necessaryReadLockRefs {
155+
lockRef.Tag = ""
156+
if lockRef.Digest == "" {
157+
continue
158+
}
159+
lockRefStr := lockRef.String()
160+
if seenChildren[lockRefStr] {
161+
continue
162+
}
163+
seenChildren[lockRefStr] = true
164+
lock, _ := childMutexes.LoadOrStore(lockRefStr, &sync.RWMutex{})
165+
lock.(*sync.RWMutex).RLock()
166+
defer lock.(*sync.RWMutex).RUnlock()
167+
}
168+
169+
logText := ref.StringWithKnownDigest(refsDigest) + logSuffix
170+
fmt.Println(startedPrefix + logText)
171+
desc, err := normal.do(ctx, ref)
172+
if err != nil {
173+
fmt.Fprintf(os.Stderr, "%s%s -- ERROR: %v\n", failurePrefix, logText, err)
174+
panic(err) // TODO exit in a more clean way (we can't use "os.Exit" because that causes *more* errors 😭)
175+
}
176+
if ref.Digest == "" && refsDigest == "" {
177+
logText += "@" + string(desc.Digest)
178+
}
179+
fmt.Println(successPrefix + logText)
180+
}()
181+
}
72182
}
183+
184+
wg.Wait()
73185
}

registry/manifest-children.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package registry
2+
3+
import (
4+
"encoding/json"
5+
6+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
7+
)
8+
9+
type ManifestChildren struct {
10+
// *technically* this should be two separate structs chosen based on mediaType (https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m), but that makes the code a lot more annoying when we're just collecting a list of potential children we need to copy over for the parent object to push successfully
11+
12+
// intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/index.go#L21 to minimize parsing
13+
Manifests []ocispec.Descriptor `json:"manifests"`
14+
15+
// intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/manifest.go#L20 to minimize parsing
16+
Config *ocispec.Descriptor `json:"config"` // have to turn this into a pointer so we can recognize when it's not set easier / more correctly
17+
Layers []ocispec.Descriptor `json:"layers"`
18+
}
19+
20+
// opportunistically parse a given manifest for any *potential* child objects; will return JSON parsing errors for non-JSON
21+
func ParseManifestChildren(manifest []byte) (ManifestChildren, error) {
22+
var manifestChildren ManifestChildren
23+
err := json.Unmarshal(manifest, &manifestChildren)
24+
return manifestChildren, err
25+
}

0 commit comments

Comments
 (0)