Repository: regclient/regclient
Pull request #548 (2023-09-27 19:28:51)

Support pushing artifact as index entry
<!--

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

<!-- If this is a bug fix, include "fixes #xxxx", or "closes #xxxx" -->

### Describe the change

This allows artifacts to be pushed as entries to an index using the `regctl artifact put --index` flag.
<!-- 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

```shell
$ echo oatmeal and orange juice \
  | regctl artifact put --annotation meal=breakfast --platform linux/amd64 \
    --artifact-type application/example.fbom --index ocidir://test/fbom:index

$ echo turkey sandwitch, bananna, and water \
  | regctl artifact put --annotation meal=lunch --platform linux/arm64 \
    --artifact-type application/example.fbom --index ocidir://test/fbom:index

$ regctl manifest get ocidir://test/fbom:index
Name:           ocidir://test/fbom:index
MediaType:      application/vnd.oci.image.index.v1+json
Digest:         sha256:95cb8dbe5267ac9ce1ea40dd2398771c46b6777a6981577e38a211fdfdb03c02
                
Manifests:      
                
  Name:         ocidir://test/fbom:index@sha256:6902eff9fb77c9f8247d7ee6615756e986ac305ced37b07bb6f37eaf495193d5
  Digest:       sha256:6902eff9fb77c9f8247d7ee6615756e986ac305ced37b07bb6f37eaf495193d5
  MediaType:    application/vnd.oci.image.manifest.v1+json
  ArtifactType: application/example.fbom
  Platform:     linux/amd64
  Annotations:  
    meal:       breakfast
                
  Name:         ocidir://test/fbom:index@sha256:5eaf4cb75485a03cdfcc90c5ddfbcd63d1381dac35e55cce19c13b4a1d624430
  Digest:       sha256:5eaf4cb75485a03cdfcc90c5ddfbcd63d1381dac35e55cce19c13b4a1d624430
  MediaType:    application/vnd.oci.image.manifest.v1+json
  ArtifactType: application/example.fbom
  Platform:     linux/arm64
  Annotations:  
    meal:       lunch

$ regctl artifact get --filter-artifact-type application/example.fbom --filter-annotation meal=breakfast ocidir://test/fbom:index
oatmeal and orange juice
```
<!-- Include steps that can be taken to verify the change -->

### Changelog text

- Support pushing artifact to an index entry with `regctl artifact put --index`
<!-- 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

<!-- markdownlint-disable-file MD041 -->



Patch:
diff --git a/cmd/regctl/artifact.go b/cmd/regctl/artifact.go
index a96425c..99e4463 100644
--- a/cmd/regctl/artifact.go
+++ b/cmd/regctl/artifact.go
@@ -106,6 +106,7 @@ var artifactOpts struct {
 	formatList       string
 	formatPut        string
 	formatTree       string
+	index            bool
 	latest           bool
 	outputDir        string
 	platform         string
@@ -164,6 +165,7 @@ func init() {
 	artifactPutCmd.Flags().StringArrayVar(&artifactOpts.annotations, "annotation", []string{}, "Annotation to include on manifest")
 	artifactPutCmd.Flags().BoolVar(&artifactOpts.byDigest, "by-digest", false, "Push manifest by digest instead of tag")
 	artifactPutCmd.Flags().StringVar(&artifactOpts.formatPut, "format", "", "Format output with go template syntax")
+	artifactPutCmd.Flags().BoolVar(&artifactOpts.index, "index", false, "Create/append artifact to an index")
 	artifactPutCmd.Flags().StringVar(&artifactOpts.subject, "subject", "", "Set the subject to a reference (used for referrer queries)")
 	artifactPutCmd.Flags().BoolVar(&artifactOpts.stripDirs, "strip-dirs", false, "Strip directories from filenames in artifact")
 	artifactPutCmd.Flags().StringVarP(&artifactOpts.platform, "platform", "p", "", "Specify platform of a subject (e.g. linux/amd64 or local)")
@@ -836,14 +838,14 @@ func runArtifactPut(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	if artifactOpts.byDigest || rArt.IsZero() {
+	if artifactOpts.byDigest || artifactOpts.index || rArt.IsZero() {
 		r.Tag = ""
 		r.Digest = mm.GetDescriptor().Digest.String()
 	}
 
 	// push manifest
 	putOpts := []regclient.ManifestOpts{}
-	if rArt.IsZero() {
+	if rArt.IsZero() || artifactOpts.index {
 		putOpts = append(putOpts, regclient.WithManifestChild())
 	}
 	err = rc.ManifestPut(ctx, r, mm, putOpts...)
@@ -851,6 +853,57 @@ func runArtifactPut(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
+	// create/append to index
+	if artifactOpts.index && !rArt.IsZero() {
+		// create a descriptor to add
+		d := mm.GetDescriptor()
+		d.ArtifactType = artifactOpts.artifactType
+		d.Annotations = annotations
+		if artifactOpts.platform != "" {
+			p, err := platform.Parse(artifactOpts.platform)
+			if err != nil {
+				return fmt.Errorf("failed to parse platform: %w", err)
+			}
+			d.Platform = &p
+		}
+		mi, err := rc.ManifestGet(ctx, rArt)
+		if err == nil && mi.IsList() {
+			// append to existing index
+			mii, ok := mi.(manifest.Indexer)
+			if !ok {
+				return fmt.Errorf("index to append to is a list but not an Indexer?")
+			}
+			dl, err := mii.GetManifestList()
+			if err != nil {
+				return err
+			}
+			dl = append(dl, d)
+			err = mii.SetManifestList(dl)
+			if err != nil {
+				return err
+			}
+			err = rc.ManifestPut(ctx, rArt, mi)
+			if err != nil {
+				return err
+			}
+		} else {
+			// create a new index
+			mii := v1.Index{
+				Versioned: v1.IndexSchemaVersion,
+				MediaType: types.MediaTypeOCI1ManifestList,
+				Manifests: []types.Descriptor{d},
+			}
+			mi, err := manifest.New(manifest.WithOrig(mii))
+			if err != nil {
+				return err
+			}
+			err = rc.ManifestPut(ctx, rArt, mi)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
 	result := struct {
 		Manifest manifest.Manifest
 	}{


Test patch:
diff --git a/cmd/regctl/artifact_test.go b/cmd/regctl/artifact_test.go
index ee67495..816b56d 100644
--- a/cmd/regctl/artifact_test.go
+++ b/cmd/regctl/artifact_test.go
@@ -206,6 +206,16 @@ func TestArtifactPut(t *testing.T) {
 			args: []string{"artifact", "put", "--artifact-type", "application/vnd.example", "--config-file", "", "--subject", "ocidir://" + testDir + ":put-example-at"},
 			in:   testData,
 		},
+		{
+			name: "Put create index",
+			args: []string{"artifact", "put", "--artifact-type", "application/vnd.example", "--config-file", "", "--annotation", "test=a", "--platform", "linux/amd64", "--index", "ocidir://" + testDir + ":index"},
+			in:   testData,
+		},
+		{
+			name: "Put append index",
+			args: []string{"artifact", "put", "--artifact-type", "application/vnd.example", "--config-file", "", "--annotation", "test=b", "--platform", "linux/arm64", "--index", "ocidir://" + testDir + ":index"},
+			in:   testData,
+		},
 	}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {