Repository: regclient/regclient (go)

Issue:
[Question] Copy image for cartain platforms
<!--
Note: Please search to see if an issue already exists for the bug you encountered.
-->

Hi, is there option to copy image between registers (image layers) only for defined platforms? 
Like `regctl image copy --platform linux/amd64,linux/arm64 <src_image_ref> <dst_image_ref>`

Or another way how to do that? Trying to avoid copy all platform images if I need only two.

### Question

<!-- Ask away, but please include enough detail since we can't see your screen from here. -->

### Version


### Environment

<!--
Please include:
- How you are running the tool: downloaded binary or from within a container
- Your platform: Linux, Windows, or Mac, x86 or ARM.
- Your registry: Docker Hub, ECR, GCR, ACR, Harbor, registry:2, etc.
-->

- Running as binary or container:
- Host platform:
- Registry description:

### Anything else

<!--
Links? References? Anything that will give us more context about the issue that you are encountering!
-->


Pull request description:
Add platform option to regctl index create/add
<!--

Commits must be signed indicating your agreement to the [DCO](https://developercertificate.org/).
See [DCO missing](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) for steps to fix a missing signoff.

-->

### Fixes issue

Fixes #294
<!-- If this is a bug fix, include "fixes #xxxx", or "closes #xxxx" -->

### Describe the change

This adds a platform option to `regctl index create` and `regctl index add`.
<!-- Include the type of change: bug fix, new feature, breaking change, documentation update -->
<!-- Describe what was changed, why the change was made, and how it was implemented -->

### How to verify it

```
# create a manifest with two platforms from the source
regctl index create \
  --ref ${src} \
  --platform linux/amd64 \
  --platform linux/arm64 \
  ${tgt}

# add 2 more platforms to an existing manifest
regctl index add \
  --ref ${src} \
  --platform linux/arm/v7 \
  --platform linux/arm/v6 \
  ${tgt}
```

<!-- Include steps that can be taken to verify the change -->

### Changelog text

Add platforms option to `regctl index add/create`
<!-- If the release changelog should have an entry for this, include it here -->

### Please verify and check that the pull request fulfills the following requirements

<!-- Mark the following with an [X] to verify they are included -->

- [X] Tests have been added or not applicable
- [X] Documentation has been added, updated, or not applicable
- [X] Changes have been rebased to main
- [X] Multiple commits to the same code have been squashed


Patch:
diff --git a/cmd/regctl/index.go b/cmd/regctl/index.go
index b76702f..1d45aaf 100644
--- a/cmd/regctl/index.go
+++ b/cmd/regctl/index.go
@@ -74,6 +74,7 @@ func init() {
 	indexAddCmd.Flags().StringVarP(&indexOpts.descPlatform, "desc-platform", "", "", "Platform to set in descriptors of new entries")
 	indexAddCmd.Flags().StringArrayVarP(&indexOpts.digests, "digest", "", []string{}, "Digest to add")
 	indexAddCmd.Flags().StringArrayVarP(&indexOpts.refs, "ref", "", []string{}, "References to add")
+	indexAddCmd.Flags().StringArrayVarP(&indexOpts.platforms, "platform", "", []string{}, "Platforms to include from ref")
 
 	indexCreateCmd.Flags().StringArrayVarP(&indexOpts.annotations, "annotation", "", []string{}, "Annotation to set on manifest")
 	indexCreateCmd.Flags().BoolVarP(&indexOpts.byDigest, "by-digest", "", false, "Push manifest by digest instead of tag")
@@ -83,6 +84,7 @@ func init() {
 	indexCreateCmd.Flags().StringVarP(&indexOpts.format, "format", "", "", "Format output with go template syntax")
 	indexCreateCmd.Flags().StringVarP(&indexOpts.mediaType, "media-type", "m", types.MediaTypeOCI1ManifestList, "Media-type for manifest list or OCI Index")
 	indexCreateCmd.Flags().StringArrayVarP(&indexOpts.refs, "ref", "", []string{}, "References to include in new index")
+	indexCreateCmd.Flags().StringArrayVarP(&indexOpts.platforms, "platform", "", []string{}, "Platforms to include from ref")
 	indexCreateCmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		return indexKnownTypes, cobra.ShellCompDirectiveNoFileComp
 	})
@@ -331,6 +333,8 @@ func runIndexDelete(cmd *cobra.Command, args []string) error {
 }
 
 func indexBuildDescList(ctx context.Context, rc *regclient.RegClient, r ref.Ref) ([]types.Descriptor, error) {
+	// TODO: set image copy opts here, support copying referrers and digest tags
+
 	descAnnotations := map[string]string{}
 	for _, a := range indexOpts.descAnnotations {
 		aSplit := strings.SplitN(a, "=", 2)
@@ -340,6 +344,14 @@ func indexBuildDescList(ctx context.Context, rc *regclient.RegClient, r ref.Ref)
 			descAnnotations[aSplit[0]] = aSplit[1]
 		}
 	}
+	platforms := []platform.Platform{}
+	for _, pStr := range indexOpts.platforms {
+		p, err := platform.Parse(pStr)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse platform %s: %w", pStr, err)
+		}
+		platforms = append(platforms, p)
+	}
 
 	// copy each ref by digest to the destination repository
 	if indexOpts.digests == nil {
@@ -354,15 +366,47 @@ func indexBuildDescList(ctx context.Context, rc *regclient.RegClient, r ref.Ref)
 		if err != nil {
 			return nil, err
 		}
-		desc := mCopy.GetDescriptor()
-		tgtRef := r
-		tgtRef.Tag = ""
-		tgtRef.Digest = desc.Digest.String()
-		err = rc.ImageCopy(ctx, srcRef, tgtRef, regclient.ImageWithChild())
-		if err != nil {
-			return nil, err
+		if !mCopy.IsList() || len(platforms) == 0 {
+			// single manifest
+			desc := mCopy.GetDescriptor()
+			tgtRef := r
+			tgtRef.Tag = ""
+			tgtRef.Digest = desc.Digest.String()
+			err = rc.ImageCopy(ctx, srcRef, tgtRef, regclient.ImageWithChild())
+			if err != nil {
+				return nil, err
+			}
+			indexOpts.digests = append(indexOpts.digests, desc.Digest.String())
+		} else {
+			// platform specific descriptors are being extracted from a manifest list
+			mCopy, err = rc.ManifestGet(ctx, srcRef)
+			if err != nil {
+				return nil, err
+			}
+			mi, ok := mCopy.(manifest.Indexer)
+			if !ok {
+				return nil, fmt.Errorf("manifest list is not an Indexer")
+			}
+			dl, err := mi.GetManifestList()
+			if err != nil {
+				return nil, fmt.Errorf("failed to get descriptor list: %w", err)
+			}
+			for _, d := range dl {
+				if d.Platform != nil && indexPlatformInList(*d.Platform, platforms) {
+					dRef := srcRef
+					dRef.Tag = ""
+					dRef.Digest = d.Digest.String()
+					tgtRef := r
+					tgtRef.Tag = ""
+					tgtRef.Digest = d.Digest.String()
+					err = rc.ImageCopy(ctx, dRef, tgtRef, regclient.ImageWithChild())
+					if err != nil {
+						return nil, err
+					}
+					indexOpts.digests = append(indexOpts.digests, d.Digest.String())
+				}
+			}
 		}
-		indexOpts.digests = append(indexOpts.digests, desc.Digest.String())
 	}
 
 	// parse each digest, pull manifest, get config, append to list of descriptors
@@ -442,3 +486,12 @@ func indexDescListRmDup(dl []types.Descriptor) []types.Descriptor {
 	}
 	return dl
 }
+
+func indexPlatformInList(p platform.Platform, pl []platform.Platform) bool {
+	for _, cur := range pl {
+		if platform.Match(p, cur) {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/cmd/regctl/index_test.go b/cmd/regctl/index_test.go
new file mode 100644
index 0000000..4aabb9f
--- /dev/null
+++ b/cmd/regctl/index_test.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestIndexCreate(t *testing.T) {
+	tmpDir := t.TempDir()
+	tmpRef := fmt.Sprintf("ocidir://%s/repo:latest", tmpDir)
+	srcRef := "ocidir://../../testdata/testrepo:v3"
+
+	// create index with 2 platforms from test repo
+	out, err := cobraTest(t, "index", "create", "--ref", srcRef, "--platform", "linux/amd64", "--platform", "linux/arm/v7", tmpRef)
+	if err != nil {
+		t.Errorf("failed to run index create: %v", err)
+		return
+	}
+	if out != "" {
+		t.Errorf("unexpected output: %v", out)
+	}
+	// verify content
+	_, err = cobraTest(t, "manifest", "get", "--platform", "linux/amd64", tmpRef)
+	if err != nil {
+		t.Errorf("failed to get linux/amd64 entry: %v", err)
+	}
+	_, err = cobraTest(t, "manifest", "get", "--platform", "linux/arm/v7", tmpRef)
+	if err != nil {
+		t.Errorf("failed to get linux/arm/v7 entry: %v", err)
+	}
+	_, err = cobraTest(t, "manifest", "get", "--platform", "linux/arm64", tmpRef)
+	if err == nil {
+		t.Errorf("found linux/arm64 entry: %v", err)
+	}
+}