Skip to content

Commit 8b4e0f3

Browse files
committed
(re-)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. When we originally introduced this, it collided with some other aging infrastructure in a bad way, kicking over everything, and had to be reverted. We've since changed that and this should be safe to re-introduce.
1 parent 4f22d05 commit 8b4e0f3

File tree

5 files changed

+194
-48
lines changed

5 files changed

+194
-48
lines changed

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+
}

registry/push.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,8 @@ func EnsureManifest(ctx context.Context, ref Reference, manifest json.RawMessage
7474
errors.Is(err, ociregistry.ErrBlobUnknown) ||
7575
(errors.As(err, &httpErr) && httpErr.StatusCode() >= 400 && httpErr.StatusCode() <= 499) {
7676
// this probably means we need to push some child manifests and/or mount missing blobs (and then retry the manifest push)
77-
var manifestChildren struct {
78-
// *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
79-
80-
// intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/index.go#L21 to minimize parsing
81-
Manifests []ocispec.Descriptor `json:"manifests"`
82-
83-
// intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/manifest.go#L20 to minimize parsing
84-
Config *ocispec.Descriptor `json:"config"` // have to turn this into a pointer so we can recognize when it's not set easier / more correctly
85-
Layers []ocispec.Descriptor `json:"layers"`
86-
}
87-
if err := json.Unmarshal(manifest, &manifestChildren); err != nil {
77+
manifestChildren, err := ParseManifestChildren(manifest)
78+
if err != nil {
8879
return desc, fmt.Errorf("%s: failed parsing manifest JSON: %w", ref, err)
8980
}
9081

0 commit comments

Comments
 (0)