From 8b4e0f376ac25529a43e4593f43b563bb39c4dce Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 23 Jan 2025 11:09:07 -0800 Subject: [PATCH 1/4] (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. --- cmd/deploy/input.go | 54 ++++++++----- cmd/deploy/input_test.go | 8 +- cmd/deploy/main.go | 142 ++++++++++++++++++++++++++++++---- registry/manifest-children.go | 25 ++++++ registry/push.go | 13 +--- 5 files changed, 194 insertions(+), 48 deletions(-) create mode 100644 registry/manifest-children.go diff --git a/cmd/deploy/input.go b/cmd/deploy/input.go index 8e8e80c..018fcec 100644 --- a/cmd/deploy/input.go +++ b/cmd/deploy/input.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "github.com/docker-library/meta-scripts/registry" @@ -46,7 +47,14 @@ type inputNormalized struct { Data []byte `json:"data"` CopyFrom *registry.Reference `json:"copyFrom"` - Do func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) `json:"-"` + // if CopyFrom is nil and Type is manifest, this will be set (used by "do") + MediaType string `json:"mediaType,omitempty"` +} + +func (normal inputNormalized) clone() inputNormalized { + // normal.Lookup is the only thing we have concurrency issues with, so it's the only thing we'll explicitly clone 😇 + normal.Lookup = maps.Clone(normal.Lookup) + return normal } func normalizeInputRefs(deployType deployType, rawRefs []string) ([]registry.Reference, ociregistry.Digest, error) { @@ -222,6 +230,7 @@ func NormalizeInput(raw inputRaw) (inputNormalized, error) { normal.Lookup[d] = ref } + // front-load some validation / data extraction for "normal.do" to work switch normal.Type { case typeManifest: if normal.CopyFrom == nil { @@ -240,33 +249,42 @@ func NormalizeInput(raw inputRaw) (inputNormalized, error) { // and our logic for pushing children needs to know the mediaType (see the GHSAs referenced above) return normal, fmt.Errorf("%s: pushing manifest but missing 'mediaType'", debugId) } - normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) { - return registry.EnsureManifest(ctx, dstRef, normal.Data, mediaTypeHaver.MediaType, normal.Lookup) - } + normal.MediaType = mediaTypeHaver.MediaType + } + + case typeBlob: + if normal.CopyFrom != nil && normal.CopyFrom.Digest == "" { + return normal, fmt.Errorf("%s: blobs are always by-digest, and thus need a digest: %s", debugId, normal.CopyFrom) + } + + default: + panic("unknown type: " + string(normal.Type)) + // panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error) + } + + return normal, nil +} + +// 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) +func (normal inputNormalized) do(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) { + switch normal.Type { + case typeManifest: + if normal.CopyFrom == nil { + // TODO panic on bad data, like MediaType being empty? + return registry.EnsureManifest(ctx, dstRef, normal.Data, normal.MediaType, normal.Lookup) } else { - normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) { - return registry.CopyManifest(ctx, *normal.CopyFrom, dstRef, normal.Lookup) - } + return registry.CopyManifest(ctx, *normal.CopyFrom, dstRef, normal.Lookup) } case typeBlob: if normal.CopyFrom == nil { - normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) { - return registry.EnsureBlob(ctx, dstRef, int64(len(normal.Data)), bytes.NewReader(normal.Data)) - } + return registry.EnsureBlob(ctx, dstRef, int64(len(normal.Data)), bytes.NewReader(normal.Data)) } else { - if normal.CopyFrom.Digest == "" { - return normal, fmt.Errorf("%s: blobs are always by-digest, and thus need a digest: %s", debugId, normal.CopyFrom) - } - normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) { - return registry.CopyBlob(ctx, *normal.CopyFrom, dstRef) - } + return registry.CopyBlob(ctx, *normal.CopyFrom, dstRef) } default: panic("unknown type: " + string(normal.Type)) // panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error) } - - return normal, nil } diff --git a/cmd/deploy/input_test.go b/cmd/deploy/input_test.go index 8304c68..51b8d75 100644 --- a/cmd/deploy/input_test.go +++ b/cmd/deploy/input_test.go @@ -281,7 +281,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example:test" ], "data": {"mediaType": "application/vnd.oci.image.index.v1+json"} }`, - `{"type":"manifest","refs":["localhost:5000/example:test@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null}`, + `{"type":"manifest","refs":["localhost:5000/example:test@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null,"mediaType":"application/vnd.oci.image.index.v1+json"}`, }, { "manifest raw", @@ -290,7 +290,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example" ], "data": "eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=" }`, - `{"type":"manifest","refs":["localhost:5000/example@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null}`, + `{"type":"manifest","refs":["localhost:5000/example@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null,"mediaType":"application/vnd.oci.image.index.v1+json"}`, }, { @@ -301,7 +301,7 @@ func TestNormalizeInput(t *testing.T) { "lookup": { "sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d": "tianon/true" }, "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} }`, - `{"type":"manifest","refs":["localhost:5000/example:test@sha256:0cb474919526d040392883b84e5babb65a149cc605b89b117781ab94e88a5e86"],"lookup":{"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIiwibWFuaWZlc3RzIjpbeyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQub2NpLmltYWdlLm1hbmlmZXN0LnYxK2pzb24iLCJkaWdlc3QiOiJzaGEyNTY6OWVmNDJmMWQ2MDJmYjQyM2ZhZDkzNWFhYzFjYWEwY2ZkYmNlMWFkN2VkY2U2NGQwODBhNGViN2IxM2Y3Y2Q5ZCIsInNpemUiOjExNjV9XSwic2NoZW1hVmVyc2lvbiI6Mn0=","copyFrom":null}`, + `{"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"}`, }, { "image", @@ -311,7 +311,7 @@ func TestNormalizeInput(t *testing.T) { "lookup": { "": "tianon/true" }, "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"}]} }`, - `{"type":"manifest","refs":["localhost:5000/example@sha256:1c70f9d471b83100c45d5a218d45bbf7e073e11ea5043758a020379a7c78f878"],"lookup":{"":"tianon/true"},"data":"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNDcxLCJkaWdlc3QiOiJzaGEyNTY6NjkwOTEyMDk0YzAxNjVjNDg5Zjg3NGM3MmNlZTRiYTIwOGMyODk5MmMwNjk5ZmE2ZTEwZDhjYzU5ZjkzZmVjOSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMjksImRpZ2VzdCI6InNoYTI1Njo0Yzc0ZDc0NDM5N2Q0YmNiZDMwNzlkOWM4MmE4N2I4MGQ0M2RhMzc2MzEzNzcyOTc4MTM0ZDEyODhmMjA1MThjIn1dfQ==","copyFrom":null}`, + `{"type":"manifest","refs":["localhost:5000/example@sha256:1c70f9d471b83100c45d5a218d45bbf7e073e11ea5043758a020379a7c78f878"],"lookup":{"":"tianon/true"},"data":"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNDcxLCJkaWdlc3QiOiJzaGEyNTY6NjkwOTEyMDk0YzAxNjVjNDg5Zjg3NGM3MmNlZTRiYTIwOGMyODk5MmMwNjk5ZmE2ZTEwZDhjYzU5ZjkzZmVjOSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMjksImRpZ2VzdCI6InNoYTI1Njo0Yzc0ZDc0NDM5N2Q0YmNiZDMwNzlkOWM4MmE4N2I4MGQ0M2RhMzc2MzEzNzcyOTc4MTM0ZDEyODhmMjA1MThjIn1dfQ==","copyFrom":null,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}`, }, { diff --git a/cmd/deploy/main.go b/cmd/deploy/main.go index 91862e6..89c812c 100644 --- a/cmd/deploy/main.go +++ b/cmd/deploy/main.go @@ -7,6 +7,11 @@ import ( "os" "os/exec" "os/signal" + "sync" + + "github.com/docker-library/meta-scripts/registry" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) func main() { @@ -32,6 +37,11 @@ func main() { panic(err) } + // a set of RWMutex objects for synchronizing the pushing of "child" objects before their parents later in the list of documents + // 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) + childMutexes := sync.Map{} + wg := sync.WaitGroup{} + dec := json.NewDecoder(stdout) for dec.More() { var raw inputRaw @@ -48,26 +58,128 @@ func main() { } refsDigest := normal.Refs[0].Digest - if normal.CopyFrom == nil { - fmt.Printf("Pushing %s %s:\n", raw.Type, refsDigest) + var logSuffix string = " (" + string(raw.Type) + ") " + if normal.CopyFrom != nil { + // normal copy (one repo/registry to another) + logSuffix = " 🤝" + logSuffix + normal.CopyFrom.String() + // "localhost:32774/test 🤝 (manifest) tianon/test@sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f" } else { - fmt.Printf("Copying %s %s:\n", raw.Type, *normal.CopyFrom) + // push (raw/embedded blob or manifest data) + logSuffix = " 🦾" + logSuffix + string(refsDigest) + // "localhost:32774/test 🦾 (blob) sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1" } + startedPrefix := "❔ " + successPrefix := "✅ " + failurePrefix := "❌ " + + // 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 + seenRefs := map[string]bool{} for _, ref := range normal.Refs { - fmt.Printf(" - %s", ref.StringWithKnownDigest(refsDigest)) - desc, err := normal.Do(ctx, ref) - if err != nil { - fmt.Fprintf(os.Stderr, " -- ERROR: %v\n", err) - os.Exit(1) - return - } - if ref.Digest == "" && refsDigest == "" { - fmt.Printf("@%s", desc.Digest) + ref := ref // https://github.com/golang/go/issues/60078 + + necessaryReadLockRefs := []registry.Reference{} + + // before parallelization, collect the pushing "child" mutex we need to lock for writing right away (but only for the first entry) + var mutex *sync.RWMutex + if ref.Digest != "" { + lockRef := ref + lockRef.Tag = "" + lockRefStr := lockRef.String() + if seenRefs[lockRefStr] { + // 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) + necessaryReadLockRefs = append(necessaryReadLockRefs, lockRef) + } else { + seenRefs[lockRefStr] = true + lock, _ := childMutexes.LoadOrStore(lockRefStr, &sync.RWMutex{}) + mutex = lock.(*sync.RWMutex) + // if we have a "child" mutex, lock it immediately so we don't create a race between inputs + mutex.Lock() // (this gets unlocked in the goroutine below) + // 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 + } } - fmt.Println() - } - fmt.Println() + // make a (deep) copy of "normal" so that we can use it in a goroutine ("normal.do" is not safe for concurrent invocation) + normal := normal.clone() + + wg.Add(1) + go func() { + defer wg.Done() + + if mutex != nil { + defer mutex.Unlock() + } + + // 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) + 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) + // explicitly ignoring errors because this might not actually be JSON (or even a manifest at all!); this is best-effort + // TODO optimize this by checking whether normal.Data matches "^\s*{.+}\s*$" first so we have some assurance it might work before we go further? + manifestChildren, _ := registry.ParseManifestChildren(normal.Data) + childDescs := []ocispec.Descriptor{} + childDescs = append(childDescs, manifestChildren.Manifests...) + if manifestChildren.Config != nil { + childDescs = append(childDescs, *manifestChildren.Config) + } + childDescs = append(childDescs, manifestChildren.Layers...) + for _, childDesc := range childDescs { + childRef := ref + childRef.Digest = childDesc.Digest + necessaryReadLockRefs = append(necessaryReadLockRefs, childRef) + + // these read locks are cheap, so let's be aggressive with our "lookup" refs too + if lookupRef, ok := normal.Lookup[childDesc.Digest]; ok { + lookupRef.Digest = childDesc.Digest + necessaryReadLockRefs = append(necessaryReadLockRefs, lookupRef) + } + if fallbackRef, ok := normal.Lookup[""]; ok { + fallbackRef.Digest = childDesc.Digest + necessaryReadLockRefs = append(necessaryReadLockRefs, fallbackRef) + } + } + } + // 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) + for digest, lookupRef := range normal.Lookup { + necessaryReadLockRefs = append(necessaryReadLockRefs, lookupRef) + if digest != lookupRef.Digest { + lookupRef.Digest = digest + necessaryReadLockRefs = append(necessaryReadLockRefs, lookupRef) + } + } + // if we're going to do a copy, we need to *also* include the artifact we're copying in our list + if normal.CopyFrom != nil { + necessaryReadLockRefs = append(necessaryReadLockRefs, *normal.CopyFrom) + } + // ok, we've built up a list, let's start grabbing (ro) mutexes + seenChildren := map[string]bool{} + for _, lockRef := range necessaryReadLockRefs { + lockRef.Tag = "" + if lockRef.Digest == "" { + continue + } + lockRefStr := lockRef.String() + if seenChildren[lockRefStr] { + continue + } + seenChildren[lockRefStr] = true + lock, _ := childMutexes.LoadOrStore(lockRefStr, &sync.RWMutex{}) + lock.(*sync.RWMutex).RLock() + defer lock.(*sync.RWMutex).RUnlock() + } + + logText := ref.StringWithKnownDigest(refsDigest) + logSuffix + fmt.Println(startedPrefix + logText) + desc, err := normal.do(ctx, ref) + if err != nil { + fmt.Fprintf(os.Stderr, "%s%s -- ERROR: %v\n", failurePrefix, logText, err) + panic(err) // TODO exit in a more clean way (we can't use "os.Exit" because that causes *more* errors 😭) + } + if ref.Digest == "" && refsDigest == "" { + logText += "@" + string(desc.Digest) + } + fmt.Println(successPrefix + logText) + }() + } } + + wg.Wait() } diff --git a/registry/manifest-children.go b/registry/manifest-children.go new file mode 100644 index 0000000..15e4c2a --- /dev/null +++ b/registry/manifest-children.go @@ -0,0 +1,25 @@ +package registry + +import ( + "encoding/json" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type ManifestChildren struct { + // *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 + + // intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/index.go#L21 to minimize parsing + Manifests []ocispec.Descriptor `json:"manifests"` + + // intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/manifest.go#L20 to minimize parsing + Config *ocispec.Descriptor `json:"config"` // have to turn this into a pointer so we can recognize when it's not set easier / more correctly + Layers []ocispec.Descriptor `json:"layers"` +} + +// opportunistically parse a given manifest for any *potential* child objects; will return JSON parsing errors for non-JSON +func ParseManifestChildren(manifest []byte) (ManifestChildren, error) { + var manifestChildren ManifestChildren + err := json.Unmarshal(manifest, &manifestChildren) + return manifestChildren, err +} diff --git a/registry/push.go b/registry/push.go index 9075efb..e5309f7 100644 --- a/registry/push.go +++ b/registry/push.go @@ -74,17 +74,8 @@ func EnsureManifest(ctx context.Context, ref Reference, manifest json.RawMessage errors.Is(err, ociregistry.ErrBlobUnknown) || (errors.As(err, &httpErr) && httpErr.StatusCode() >= 400 && httpErr.StatusCode() <= 499) { // this probably means we need to push some child manifests and/or mount missing blobs (and then retry the manifest push) - var manifestChildren struct { - // *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 - - // intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/index.go#L21 to minimize parsing - Manifests []ocispec.Descriptor `json:"manifests"` - - // intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/manifest.go#L20 to minimize parsing - Config *ocispec.Descriptor `json:"config"` // have to turn this into a pointer so we can recognize when it's not set easier / more correctly - Layers []ocispec.Descriptor `json:"layers"` - } - if err := json.Unmarshal(manifest, &manifestChildren); err != nil { + manifestChildren, err := ParseManifestChildren(manifest) + if err != nil { return desc, fmt.Errorf("%s: failed parsing manifest JSON: %w", ref, err) } From 2bdcc09e46f44c8cb0ca14daf3432b98ae49bd6c Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 31 Jan 2025 09:45:54 -0800 Subject: [PATCH 2/4] Implement explicit `--parallel` flag I'm working on a `--dry-run` flag that will improve the coverage of this, but wanted separate commits. --- .test/test.sh | 2 +- Jenkinsfile.deploy | 2 +- cmd/deploy/main.go | 30 +++++++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.test/test.sh b/.test/test.sh index 2da0a27..1d48a75 100755 --- a/.test/test.sh +++ b/.test/test.sh @@ -250,7 +250,7 @@ if [ -n "$doDeploy" ]; then empty ')" # stored in a variable for easier debugging ("bash -x") - time "$coverage/bin/deploy" <<<"$json" + time "$coverage/bin/deploy" --parallel <<<"$json" docker rm -vf meta-scripts-test-registry trap - EXIT diff --git a/Jenkinsfile.deploy b/Jenkinsfile.deploy index 2e8297e..fce3956 100644 --- a/Jenkinsfile.deploy +++ b/Jenkinsfile.deploy @@ -106,7 +106,7 @@ node('put-shared') { ansiColor('xterm') { ./.go-env.sh go build -trimpath -o bin/deploy ./cmd/deploy fi ) - .scripts/bin/deploy < filtered-deploy.json + .scripts/bin/deploy --parallel < filtered-deploy.json ''' } } diff --git a/cmd/deploy/main.go b/cmd/deploy/main.go index 89c812c..83d179b 100644 --- a/cmd/deploy/main.go +++ b/cmd/deploy/main.go @@ -18,7 +18,25 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() - // TODO --dry-run ? + var ( + args = os.Args[1:] + + // --parallel + parallel bool + ) + for len(args) > 0 { + arg := args[0] + args = args[1:] + + // TODO --dry-run + switch arg { + case "--parallel": + parallel = true + + default: + panic("unknown argument: " + arg) + } + } // TODO the best we can do on whether or not this actually updated tags is "yes, definitely (we had to copy some children)" and "maybe (we didn't have to copy any children)", but we should maybe still output those so we can trigger put-shared based on them (~immediately on "definitely" and with some medium delay on "maybe") @@ -103,7 +121,8 @@ func main() { normal := normal.clone() wg.Add(1) - go func() { + // (making a function instead of direct "go func() ..." so we can support the --parallel toggle) + f := func() { defer wg.Done() if mutex != nil { @@ -177,7 +196,12 @@ func main() { logText += "@" + string(desc.Digest) } fmt.Println(successPrefix + logText) - }() + } + if parallel { + go f() + } else { + f() + } } } From 10f3b0890499ad2bfbe852bd0ab591f03ed56656 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 31 Jan 2025 10:06:48 -0800 Subject: [PATCH 3/4] Add `--dry-run` flag This swaps the output to be input-compatible such that we can verify the results of a deploy, especially our new "filtered deploy" (we can verify that the filtering was warranted, but out-of-band instead of in-band). This also allows us to do more/better testing (see `.test` changes, especially `test.sh`). --- .test/deploy-dry-run-test.json | 385 +++++++++++++++++++++++++++++++++ .test/test.sh | 8 + cmd/deploy/input.go | 60 ++++- cmd/deploy/input_test.go | 30 +-- cmd/deploy/main.go | 69 +++++- 5 files changed, 524 insertions(+), 28 deletions(-) create mode 100644 .test/deploy-dry-run-test.json diff --git a/.test/deploy-dry-run-test.json b/.test/deploy-dry-run-test.json new file mode 100644 index 0000000..43728a6 --- /dev/null +++ b/.test/deploy-dry-run-test.json @@ -0,0 +1,385 @@ +{ + "type": "blob", + "refs": [ + "localhost:3000/test@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1" + ], + "data": "YnVmZnkgdGhlIHZhbXBpcmUgc2xheWVyCg==" +} +{ + "type": "blob", + "refs": [ + "localhost:3000/test@sha256:bdc1ce731138e680ada95089dded3015b8e1570d9a70216867a2a29801a747b3" + ], + "data": "ewogICJmb28iOiAiYmFyIiwKICAiYmF6IjogWwogICAgImJ1enoiLAogICAgImJ1enoiLAogICAgImJ1enoiCiAgXQp9Cg==" +} +{ + "type": "blob", + "refs": [ + "localhost:3000/test@sha256:680c1729a6d4a34f69123f5936cfd4f2cb82a008951241cfc499f9e52996b380" + ], + "data": "Impzb24gc3RyaW5nIgo=" +} +{ + "type": "blob", + "refs": [ + "localhost:3000/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e" + ], + "data": "ewoJImFyY2hpdGVjdHVyZSI6ICJhbWQ2NCIsCgkiY29uZmlnIjogewoJCSJDbWQiOiBbCgkJCSIvdHJ1ZSIKCQldCgl9LAoJImNyZWF0ZWQiOiAiMjAyMy0wMi0wMVQwNjo1MToxMVoiLAoJImhpc3RvcnkiOiBbCgkJewoJCQkiY3JlYXRlZCI6ICIyMDIzLTAyLTAxVDA2OjUxOjExWiIsCgkJCSJjcmVhdGVkX2J5IjogImh0dHBzOi8vZ2l0aHViLmNvbS90aWFub24vZG9ja2VyZmlsZXMvdHJlZS9tYXN0ZXIvdHJ1ZSIKCQl9CgldLAoJIm9zIjogImxpbnV4IiwKCSJyb290ZnMiOiB7CgkJImRpZmZfaWRzIjogWwoJCQkic2hhMjU2OjY1YjVhNDU5M2NjNjFkM2VhNmQzNTVmYjk3YzA0MzBkODIwZWUyMWFhODUzNWY1ZGU0NWU3NWMzMTk1NGI3NDMiCgkJXSwKCQkidHlwZSI6ICJsYXllcnMiCgl9Cn0K" +} +{ + "type": "blob", + "refs": [ + "localhost:3000/true@sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6" + ], + "data": "H4sIAAAAAAACAyspKk1loDEwAAJTU1MwDQTotIGhuQmcDRE3MzM0YlAwYKADKC0uSSxSUGAYoaDe1ceNiZERzmdisGMA8SoYHMB8Byx6HBgsGGA6QDQrmiwyXQPl1cDlIUG9wYaflWEUDDgAAIAGdJIABAAA" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:oci@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:oci@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:latest@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:latest@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:0@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:0@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:1@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:1@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:2@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:2@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:3@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:3@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:4@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:4@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:5@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:5@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:6@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:6@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:7@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:7@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:8@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:8@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/true:9@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/foo/true:9@sha256:4a4149cb9b710b845e9e6b57d66408ca84472937a8aa431b4dddd1e6b122e9f2" + ], + "lookup": { + "": "localhost:3000/true", + "sha256:1c51fc286aa95d9413226599576bafa38490b1e292375c90de095855b64caea6": "localhost:3000/true" + }, + "data": "ewogICJzY2hlbWFWZXJzaW9uIjogMiwKICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsCiAgImNvbmZpZyI6IHsKICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5jb25maWcudjEranNvbiIsCiAgICAiZGlnZXN0IjogInNoYTI1NjoyNWJlODIyNTMzMzZmMGI4YzQzNDdiYzRlY2JiY2RjODVkMGUwZjExOGNjZjhkYzJlMTE5YzBhNDdhMGE0ODZlIiwKICAgICJzaXplIjogMzk2CiAgfSwKICAibGF5ZXJzIjogWwogICAgewogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubGF5ZXIudjEudGFyK2d6aXAiLAogICAgICAiZGlnZXN0IjogInNoYTI1NjoxYzUxZmMyODZhYTk1ZDk0MTMyMjY1OTk1NzZiYWZhMzg0OTBiMWUyOTIzNzVjOTBkZTA5NTg1NWI2NGNhZWE2IiwKICAgICAgInNpemUiOiAxMTcKICAgIH0KICBdCn0K", + "mediaType": "application/vnd.oci.image.manifest.v1+json" +} +{ + "type": "blob", + "refs": [ + "localhost:3000/test-mount@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1" + ], + "lookup": { + "": "localhost:3000/test" + }, + "copyFrom": "localhost:3000/test@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1" +} +{ + "type": "blob", + "refs": [ + "localhost:3000/cirros@sha256:6cef03f2716ee8ba76999750aee1a742888ccd0db923be33ff6a410d87f4277d" + ], + "lookup": { + "": "oisupport/staging-amd64" + }, + "copyFrom": "oisupport/staging-amd64@sha256:6cef03f2716ee8ba76999750aee1a742888ccd0db923be33ff6a410d87f4277d" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/cirros" + ], + "lookup": { + "": "oisupport/staging-amd64" + }, + "copyFrom": "oisupport/staging-amd64:34bb44c7d8b6fb7a337fcee0afa7c3a84148e35db6ab83041714c3e6d4c6238b" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/test@sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f" + ], + "lookup": { + "": "tianon/test", + "sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f": "tianon/test" + }, + "copyFrom": "tianon/test@sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/test@sha256:96a7a809d1b336011450164564154a5e1c257dc7eb9081e28638537c472ccb90" + ], + "lookup": { + "": "tianon/test", + "sha256:96a7a809d1b336011450164564154a5e1c257dc7eb9081e28638537c472ccb90": "tianon/test" + }, + "copyFrom": "tianon/test@sha256:96a7a809d1b336011450164564154a5e1c257dc7eb9081e28638537c472ccb90" +} +{ + "type": "manifest", + "refs": [ + "localhost:3000/test:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee@sha256:73614cc99c500aa4fa061368ed349df24a81844e3c2e6d0c31f290a7c8d73c22" + ], + "lookup": { + "": "tianon/test" + }, + "copyFrom": "tianon/test:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee@sha256:73614cc99c500aa4fa061368ed349df24a81844e3c2e6d0c31f290a7c8d73c22" +} diff --git a/.test/test.sh b/.test/test.sh index 1d48a75..4d5f884 100755 --- a/.test/test.sh +++ b/.test/test.sh @@ -250,8 +250,16 @@ if [ -n "$doDeploy" ]; then empty ')" # stored in a variable for easier debugging ("bash -x") + time "$coverage/bin/deploy" --dry-run --parallel <<<"$json" > "$dir/deploy-dry-run-test.json" + # port is random, so let's de-randomize it: + sed -i -e "s/localhost:$registryPort/localhost:3000/g" "$dir/deploy-dry-run-test.json" + time "$coverage/bin/deploy" --parallel <<<"$json" + # now that we're done with deploying, a second dry-run should come back empty (this time without parallel to test other codepaths) + time empty="$("$coverage/bin/deploy" --dry-run <<<"$json")" + ( set -x; test -z "$empty" ) + docker rm -vf meta-scripts-test-registry trap - EXIT fi diff --git a/cmd/deploy/input.go b/cmd/deploy/input.go index 018fcec..f4b1227 100644 --- a/cmd/deploy/input.go +++ b/cmd/deploy/input.go @@ -44,8 +44,8 @@ type inputNormalized struct { Lookup map[ociregistry.Digest]registry.Reference `json:"lookup,omitempty"` // Data and CopyFrom are mutually exclusive - Data []byte `json:"data"` - CopyFrom *registry.Reference `json:"copyFrom"` + Data []byte `json:"data,omitempty"` + CopyFrom *registry.Reference `json:"copyFrom,omitempty"` // if CopyFrom is nil and Type is manifest, this will be set (used by "do") MediaType string `json:"mediaType,omitempty"` @@ -288,3 +288,59 @@ func (normal inputNormalized) do(ctx context.Context, dstRef registry.Reference) // panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error) } } + +// "do", but doesn't mutate state at all (just tells us whether "do" would've done anything) +func (normal inputNormalized) dryRun(ctx context.Context, dstRef registry.Reference) (bool, error) { + targetDigest := dstRef.Digest + var lookupType registry.LookupType + switch normal.Type { + case typeManifest: + lookupType = registry.LookupTypeManifest + if targetDigest == "" { + // 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 + r, err := registry.Lookup(ctx, *normal.CopyFrom, ®istry.LookupOptions{ + Type: lookupType, + Head: true, + }) + if err != nil { + return true, err + } + if r == nil { + return true, fmt.Errorf("%s: manifest-to-copy (%s) is 404", dstRef.String(), normal.CopyFrom.String()) + } + targetDigest = r.Descriptor().Digest + r.Close() + if targetDigest == "" { + return true, fmt.Errorf("%s: manifest-to-copy (%s) is missing digest!", dstRef.String(), normal.CopyFrom.String()) + } + if dstRef.Tag == "" { + // 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 + dstRef.Digest = targetDigest + } + } + case typeBlob: + lookupType = registry.LookupTypeBlob + if targetDigest == "" { + // see validation above in normalization + panic("blob ref missing digest, this should never happen: " + dstRef.String()) + } + default: + panic("unknown type: " + string(normal.Type)) + // panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error) + } + + r, err := registry.Lookup(ctx, dstRef, ®istry.LookupOptions{ + Type: lookupType, + Head: true, + }) + if err != nil { + return true, err + } + if r == nil { + // 404! + return true, nil + } + dstDigest := r.Descriptor().Digest + r.Close() + return targetDigest != dstDigest, nil +} diff --git a/cmd/deploy/input_test.go b/cmd/deploy/input_test.go index 51b8d75..d0d18f2 100644 --- a/cmd/deploy/input_test.go +++ b/cmd/deploy/input_test.go @@ -281,7 +281,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example:test" ], "data": {"mediaType": "application/vnd.oci.image.index.v1+json"} }`, - `{"type":"manifest","refs":["localhost:5000/example:test@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null,"mediaType":"application/vnd.oci.image.index.v1+json"}`, + `{"type":"manifest","refs":["localhost:5000/example:test@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","mediaType":"application/vnd.oci.image.index.v1+json"}`, }, { "manifest raw", @@ -290,7 +290,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example" ], "data": "eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=" }`, - `{"type":"manifest","refs":["localhost:5000/example@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null,"mediaType":"application/vnd.oci.image.index.v1+json"}`, + `{"type":"manifest","refs":["localhost:5000/example@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","mediaType":"application/vnd.oci.image.index.v1+json"}`, }, { @@ -301,7 +301,7 @@ func TestNormalizeInput(t *testing.T) { "lookup": { "sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d": "tianon/true" }, "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} }`, - `{"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"}`, + `{"type":"manifest","refs":["localhost:5000/example:test@sha256:0cb474919526d040392883b84e5babb65a149cc605b89b117781ab94e88a5e86"],"lookup":{"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIiwibWFuaWZlc3RzIjpbeyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQub2NpLmltYWdlLm1hbmlmZXN0LnYxK2pzb24iLCJkaWdlc3QiOiJzaGEyNTY6OWVmNDJmMWQ2MDJmYjQyM2ZhZDkzNWFhYzFjYWEwY2ZkYmNlMWFkN2VkY2U2NGQwODBhNGViN2IxM2Y3Y2Q5ZCIsInNpemUiOjExNjV9XSwic2NoZW1hVmVyc2lvbiI6Mn0=","mediaType":"application/vnd.oci.image.index.v1+json"}`, }, { "image", @@ -311,7 +311,7 @@ func TestNormalizeInput(t *testing.T) { "lookup": { "": "tianon/true" }, "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"}]} }`, - `{"type":"manifest","refs":["localhost:5000/example@sha256:1c70f9d471b83100c45d5a218d45bbf7e073e11ea5043758a020379a7c78f878"],"lookup":{"":"tianon/true"},"data":"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNDcxLCJkaWdlc3QiOiJzaGEyNTY6NjkwOTEyMDk0YzAxNjVjNDg5Zjg3NGM3MmNlZTRiYTIwOGMyODk5MmMwNjk5ZmE2ZTEwZDhjYzU5ZjkzZmVjOSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMjksImRpZ2VzdCI6InNoYTI1Njo0Yzc0ZDc0NDM5N2Q0YmNiZDMwNzlkOWM4MmE4N2I4MGQ0M2RhMzc2MzEzNzcyOTc4MTM0ZDEyODhmMjA1MThjIn1dfQ==","copyFrom":null,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}`, + `{"type":"manifest","refs":["localhost:5000/example@sha256:1c70f9d471b83100c45d5a218d45bbf7e073e11ea5043758a020379a7c78f878"],"lookup":{"":"tianon/true"},"data":"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNDcxLCJkaWdlc3QiOiJzaGEyNTY6NjkwOTEyMDk0YzAxNjVjNDg5Zjg3NGM3MmNlZTRiYTIwOGMyODk5MmMwNjk5ZmE2ZTEwZDhjYzU5ZjkzZmVjOSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMjksImRpZ2VzdCI6InNoYTI1Njo0Yzc0ZDc0NDM5N2Q0YmNiZDMwNzlkOWM4MmE4N2I4MGQ0M2RhMzc2MzEzNzcyOTc4MTM0ZDEyODhmMjA1MThjIn1dfQ==","mediaType":"application/vnd.docker.distribution.manifest.v2+json"}`, }, { @@ -321,7 +321,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1" ], "data": "YnVmZnkgdGhlIHZhbXBpcmUgc2xheWVyCg==" }`, - `{"type":"blob","refs":["localhost:5000/example@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1"],"data":"YnVmZnkgdGhlIHZhbXBpcmUgc2xheWVyCg==","copyFrom":null}`, + `{"type":"blob","refs":["localhost:5000/example@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1"],"data":"YnVmZnkgdGhlIHZhbXBpcmUgc2xheWVyCg=="}`, }, { "blob json", @@ -331,7 +331,7 @@ func TestNormalizeInput(t *testing.T) { "data": { } }`, - `{"type":"blob","refs":["localhost:5000/example@sha256:d914176fd50bd7f565700006a31aa97b79d3ad17cee20c8e5ff2061d5cb74817"],"data":"ewp9Cg==","copyFrom":null}`, + `{"type":"blob","refs":["localhost:5000/example@sha256:d914176fd50bd7f565700006a31aa97b79d3ad17cee20c8e5ff2061d5cb74817"],"data":"ewp9Cg=="}`, }, { @@ -341,7 +341,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example" ], "lookup": { "sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d": "tianon/true" } }`, - `{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true","sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, + `{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true","sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, }, { "copy manifest (fallback lookup)", @@ -350,7 +350,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example" ], "lookup": { "": "tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d" } }`, - `{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, + `{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, }, { "copy manifest (ref digest+fallback)", @@ -359,7 +359,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d" ], "lookup": { "": "tianon/true" } }`, - `{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, + `{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, }, { "copy manifest (tag)", @@ -368,7 +368,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example:test" ], "lookup": { "": "tianon/true:oci" } }`, - `{"type":"manifest","refs":["localhost:5000/example:test"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true:oci"}`, + `{"type":"manifest","refs":["localhost:5000/example:test"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true:oci"}`, }, { @@ -378,7 +378,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example" ], "lookup": { "sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e": "tianon/true" } }`, - `{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true","sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`, + `{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true","sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e":"tianon/true"},"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`, }, { "copy blob (fallback lookup)", @@ -387,7 +387,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example" ], "lookup": { "": "tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e" } }`, - `{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`, + `{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`, }, { "copy blob (ref digest+fallback)", @@ -396,7 +396,7 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e" ], "lookup": { "": "tianon/true" } }`, - `{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`, + `{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`, }, { @@ -410,7 +410,7 @@ func TestNormalizeInput(t *testing.T) { ], "lookup": { "": "tianon/true" } }`, - `{"type":"manifest","refs":["localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/bar@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, + `{"type":"manifest","refs":["localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/bar@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, }, { "multiple refs + multiple lookup (copy)", @@ -426,7 +426,7 @@ func TestNormalizeInput(t *testing.T) { "sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e": "tianon/true" } }`, - `{"type":"manifest","refs":["localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/bar@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true","sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e":"tianon/true","sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, + `{"type":"manifest","refs":["localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/bar@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true","sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e":"tianon/true","sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, }, } { x := x // https://github.com/golang/go/issues/60078 diff --git a/cmd/deploy/main.go b/cmd/deploy/main.go index 83d179b..5d0e7f4 100644 --- a/cmd/deploy/main.go +++ b/cmd/deploy/main.go @@ -21,6 +21,10 @@ func main() { var ( args = os.Args[1:] + // --dry-run + dryRun bool + dryRunOuts chan chan []byte + // --parallel parallel bool ) @@ -28,8 +32,12 @@ func main() { arg := args[0] args = args[1:] - // TODO --dry-run switch arg { + case "--dry-run": + dryRun = true + // we want to allow parallel, but want the output to be in-order so we resynchronize output with a channel of channels (technically this also limits parallelization, but hopefully this limit is generous enough that it doesn't matter) + dryRunOuts = make(chan chan []byte, 1000) + case "--parallel": parallel = true @@ -120,6 +128,12 @@ func main() { // make a (deep) copy of "normal" so that we can use it in a goroutine ("normal.do" is not safe for concurrent invocation) normal := normal.clone() + var dryRunOut chan []byte + if dryRun { + dryRunOut = make(chan []byte, 1) + dryRunOuts <- dryRunOut + } + wg.Add(1) // (making a function instead of direct "go func() ..." so we can support the --parallel toggle) f := func() { @@ -129,6 +143,10 @@ func main() { defer mutex.Unlock() } + if dryRun { + defer close(dryRunOut) + } + // 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) 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) // explicitly ignoring errors because this might not actually be JSON (or even a manifest at all!); this is best-effort @@ -185,17 +203,34 @@ func main() { defer lock.(*sync.RWMutex).RUnlock() } - logText := ref.StringWithKnownDigest(refsDigest) + logSuffix - fmt.Println(startedPrefix + logText) - desc, err := normal.do(ctx, ref) - if err != nil { - fmt.Fprintf(os.Stderr, "%s%s -- ERROR: %v\n", failurePrefix, logText, err) - panic(err) // TODO exit in a more clean way (we can't use "os.Exit" because that causes *more* errors 😭) - } - if ref.Digest == "" && refsDigest == "" { - logText += "@" + string(desc.Digest) + if dryRun { + needsDeploy, err := normal.dryRun(ctx, ref) + if err != nil { + fmt.Fprintf(os.Stderr, "%s -- ERROR: %v\n", failurePrefix+ref.String()+logSuffix, err) + panic(err) // TODO exit in a more clean way (we can't use "os.Exit" because that causes *more* errors 😭) + } + if needsDeploy { + normal.Refs = []registry.Reference{ref} + j, err := json.MarshalIndent(normal, "", "\t") + if err != nil { + fmt.Fprintf(os.Stderr, "%s -- JSON ERROR: %v\n", failurePrefix+ref.String()+logSuffix, err) + panic(err) // TODO exit in a more clean way (we can't use "os.Exit" because that causes *more* errors 😭) + } + dryRunOut <- j + } + } else { + logText := ref.StringWithKnownDigest(refsDigest) + logSuffix + fmt.Println(startedPrefix + logText) + desc, err := normal.do(ctx, ref) + if err != nil { + fmt.Fprintf(os.Stderr, "%s%s -- ERROR: %v\n", failurePrefix, logText, err) + panic(err) // TODO exit in a more clean way (we can't use "os.Exit" because that causes *more* errors 😭) + } + if ref.Digest == "" && refsDigest == "" { + logText += "@" + string(desc.Digest) + } + fmt.Println(successPrefix + logText) } - fmt.Println(successPrefix + logText) } if parallel { go f() @@ -205,5 +240,17 @@ func main() { } } + if dryRun { + close(dryRunOuts) + for dryRunOut := range dryRunOuts { + j, ok := <-dryRunOut + if !ok { + // (I think) this means we didn't output anything, so this should be all our "skips" + continue + } + fmt.Printf("%s\n", j) + } + } + wg.Wait() } From dfb595a41e7f964a643a64c539586bf14a82d0a9 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Mon, 3 Feb 2025 15:28:55 -0800 Subject: [PATCH 4/4] Adjust deploy input normalization produce valid inputs The new `deploy --dry-run` feature relies on the assumption that JSON-ified normalized input objects are valid input back, which wasn't an assumption we were validating. This adjusts `TestNormalizeInput` to take the output of normalization and pass it back through for an added "roundtrip" test. In doing so, I found a single edge case where our normalized objects were *not* valid/correct inputs (tag->tag copy of a manifest) and made a minor adjustment to the normalization to close that hole. I also added a new test case for that edge case, and made sure *it* round-trips correctly too. --- .test/deploy-dry-run-test.json | 4 +-- cmd/deploy/input.go | 9 ++++++ cmd/deploy/input_test.go | 57 ++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/.test/deploy-dry-run-test.json b/.test/deploy-dry-run-test.json index 43728a6..640121a 100644 --- a/.test/deploy-dry-run-test.json +++ b/.test/deploy-dry-run-test.json @@ -347,7 +347,7 @@ "localhost:3000/cirros" ], "lookup": { - "": "oisupport/staging-amd64" + "": "oisupport/staging-amd64:34bb44c7d8b6fb7a337fcee0afa7c3a84148e35db6ab83041714c3e6d4c6238b" }, "copyFrom": "oisupport/staging-amd64:34bb44c7d8b6fb7a337fcee0afa7c3a84148e35db6ab83041714c3e6d4c6238b" } @@ -381,5 +381,5 @@ "lookup": { "": "tianon/test" }, - "copyFrom": "tianon/test:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee@sha256:73614cc99c500aa4fa061368ed349df24a81844e3c2e6d0c31f290a7c8d73c22" + "copyFrom": "tianon/test@sha256:73614cc99c500aa4fa061368ed349df24a81844e3c2e6d0c31f290a7c8d73c22" } diff --git a/cmd/deploy/input.go b/cmd/deploy/input.go index f4b1227..a3191f9 100644 --- a/cmd/deploy/input.go +++ b/cmd/deploy/input.go @@ -223,8 +223,17 @@ func NormalizeInput(raw inputRaw) (inputNormalized, error) { normal.Refs[i].Digest = refsDigest } + // if we have a digest and we're performing a copy, the tag we're copying *from* is no longer relevant information + if refsDigest != "" && normal.CopyFrom != nil { + normal.CopyFrom.Tag = "" + } + // explicitly clear tag and digest from lookup entries (now that we've inferred any "CopyFrom" out of them, they no longer have any meaning) for d, ref := range normal.Lookup { + if d == "" && refsDigest == "" && ref.Tag != "" && normal.CopyFrom != nil && ref.Tag == normal.CopyFrom.Tag { + // 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) + continue + } ref.Tag = "" ref.Digest = "" normal.Lookup[d] = ref diff --git a/cmd/deploy/input_test.go b/cmd/deploy/input_test.go index d0d18f2..3717c8f 100644 --- a/cmd/deploy/input_test.go +++ b/cmd/deploy/input_test.go @@ -368,7 +368,16 @@ func TestNormalizeInput(t *testing.T) { "refs": [ "localhost:5000/example:test" ], "lookup": { "": "tianon/true:oci" } }`, - `{"type":"manifest","refs":["localhost:5000/example:test"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true:oci"}`, + `{"type":"manifest","refs":["localhost:5000/example:test"],"lookup":{"":"tianon/true:oci"},"copyFrom":"tianon/true:oci"}`, + }, + { + "copy manifest (tag and digest)", + `{ + "type": "manifest", + "refs": [ "localhost:5000/example:test" ], + "lookup": { "": "tianon/true:oci@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d" } + }`, + `{"type":"manifest","refs":["localhost:5000/example:test@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`, }, { @@ -431,19 +440,41 @@ func TestNormalizeInput(t *testing.T) { } { x := x // https://github.com/golang/go/issues/60078 t.Run(x.name, func(t *testing.T) { - var raw inputRaw - if err := json.Unmarshal([]byte(x.raw), &raw); err != nil { - t.Fatalf("JSON parse error: %v", err) - } - normal, err := NormalizeInput(raw) - if err != nil { - t.Fatalf("normalize error: %v", err) - } - if b, err := json.Marshal(normal); err != nil { - t.Fatalf("JSON generate error: %v", err) - } else if string(b) != x.normal { - t.Fatalf("got:\n%s\n\nexpected:\n%s\n", string(b), x.normal) + var b []byte // this will hold the final "normalized" JSON (so we can test round-trip afterwards) + + { // start a sub-block to ensure variable scoping is clean and roundtrip has to error if there's a typo + var raw inputRaw + if err := json.Unmarshal([]byte(x.raw), &raw); err != nil { + t.Fatalf("JSON parse error: %v", err) + } + normal, err := NormalizeInput(raw) + if err != nil { + t.Fatalf("normalize error: %v", err) + } + b, err = json.Marshal(normal) + if err != nil { + t.Fatalf("JSON generate error: %v", err) + } else if string(b) != x.normal { + t.Fatalf("got:\n%s\n\nexpected:\n%s\n", string(b), x.normal) + } } + + t.Run("roundtrip", func(t *testing.T) { + // now that we've tested that, let's round trip the normalized copy back through the normalizer to make sure it's valid/correctly parsed input too ("deploy --dry-run" leans on that assumption) + var raw inputRaw + if err := json.Unmarshal(b, &raw); err != nil { + t.Fatalf("JSON parse error: %v", err) + } + normal, err := NormalizeInput(raw) + if err != nil { + t.Fatalf("normalize error: %v", err) + } + if roundtripB, err := json.Marshal(normal); err != nil { + t.Fatalf("JSON generate error: %v", err) + } else if string(roundtripB) != x.normal { + t.Fatalf("got:\n%s\n\nexpected:\n%s\n", string(roundtripB), x.normal) + } + }) }) } }