Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: --annotation for index create #1499

Merged
merged 15 commits into from
Sep 23, 2024
37 changes: 32 additions & 5 deletions cmd/oras/root/manifest/index/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ type createOptions struct {
option.Target
option.Pretty

sources []string
extraRefs []string
outputPath string
sources []string
extraRefs []string
rawAnnotations []string
indexAnnotations map[string]string
outputPath string
}

func createCmd() *cobra.Command {
Expand All @@ -72,6 +74,9 @@ Example - Create an index from source manifests using both tags and digests, and
Example - Create an index and push it with multiple tags:
oras manifest index create localhost:5000/hello:tag1,tag2,tag3 linux-amd64 linux-arm64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9

Example - Create and push an index with annotations:
oras manifest index create localhost:5000/hello:v1 linux-amd64 --annotation "key=val"

Example - Create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1':
oras manifest index create layout-dir:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9

Expand All @@ -87,6 +92,11 @@ Example - Create an index and output the index to stdout, auto push will be disa
opts.RawReference = refs[0]
opts.extraRefs = refs[1:]
opts.sources = args[1:]
var err error
opts.indexAnnotations, err = parseAnnotations(opts.rawAnnotations)
if err != nil {
return err
}
return option.Parse(cmd, &opts)
},
Aliases: []string{"pack"},
Expand All @@ -95,6 +105,7 @@ Example - Create an index and output the index to stdout, auto push will be disa
},
}
cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the created index to, use - for stdout")
cmd.Flags().StringArrayVarP(&opts.rawAnnotations, "annotation", "a", nil, "index annotations")
option.ApplyFlags(&opts, cmd.Flags())
return oerrors.Command(cmd, &opts.Target)
}
Expand All @@ -113,8 +124,9 @@ func createIndex(cmd *cobra.Command, opts createOptions) error {
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
Annotations: opts.indexAnnotations,
}
indexBytes, err := json.Marshal(index)
if err != nil {
Expand Down Expand Up @@ -204,3 +216,18 @@ func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor,
}
return printer.Println("Digest:", desc.Digest)
}

func parseAnnotations(input []string) (map[string]string, error) {
qweeah marked this conversation as resolved.
Show resolved Hide resolved
annotations := make(map[string]string)
for _, anno := range input {
key, val, success := strings.Cut(anno, "=")
if !success {
return nil, fmt.Errorf("annotation value doesn't match the required format of \"key=value\"")
}
if _, ok := annotations[key]; ok {
return nil, fmt.Errorf("duplicate annotation key: %v", key)
}
annotations[key] = val
}
return annotations, nil
}
61 changes: 61 additions & 0 deletions cmd/oras/root/manifest/index/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package index

import (
"reflect"
"testing"
)

func Test_parseAnnotations(t *testing.T) {
tests := []struct {
name string
input []string
annotations map[string]string
wantErr bool
wantAnnotations map[string]string
}{
{
name: "valid input",
input: []string{"a=b", "c=d", "e=f"},
wantErr: false,
wantAnnotations: map[string]string{"a": "b", "c": "d", "e": "f"},
},
{
name: "invalid input",
input: []string{"a=b", "c:d", "e=f"},
wantErr: true,
wantAnnotations: nil,
},
{
name: "duplicate key",
input: []string{"a=b", "c=d", "a=e"},
wantErr: true,
wantAnnotations: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
annotations, err := parseAnnotations(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseAnnotations() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(annotations, tt.wantAnnotations) {
t.Errorf("parseAnnotations() annotations = %v, want %v", tt.annotations, tt.wantAnnotations)
}
})
}
}
34 changes: 34 additions & 0 deletions test/e2e/suite/command/manifest_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@ var _ = Describe("1.1 registry users:", func() {
ValidateIndex(content, expectedManifests)
})

It("should create index with annotations", func() {
testRepo := indexTestRepo("create", "with-annotations")
key := "image-anno-key"
value := "image-anno-value"
CopyZOTRepo(ImageRepo, testRepo)
ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "v1"), "--annotation", fmt.Sprintf("%s=%s", key, value)).Exec()
// verify
content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "v1")).Exec().Out.Contents()
var manifest ocispec.Manifest
Expect(json.Unmarshal(content, &manifest)).ShouldNot(HaveOccurred())
Expect(manifest.Annotations[key]).To(Equal(value))
})

It("should output created index to file", func() {
testRepo := indexTestRepo("create", "output-to-file")
CopyZOTRepo(ImageRepo, testRepo)
Expand All @@ -159,6 +172,14 @@ var _ = Describe("1.1 registry users:", func() {
"does-not-exist").ExpectFailure().
MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec()
})

It("should fail if given annotation input of wrong format", func() {
testRepo := indexTestRepo("create", "bad-annotations")
CopyZOTRepo(ImageRepo, testRepo)
ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""),
string(multi_arch.LinuxAMD64.Digest), "-a", "foo:bar").ExpectFailure().
MatchErrKeyWords("Error", "annotation value doesn't match the required format").Exec()
})
})

When("running `manifest index update`", func() {
Expand Down Expand Up @@ -374,6 +395,19 @@ var _ = Describe("OCI image layout users:", func() {
ValidateIndex(content, expectedManifests)
})

It("should create index with annotations", func() {
root := PrepareTempOCI(ImageRepo)
indexRef := LayoutRef(root, "with-annotations")
key := "image-anno-key"
value := "image-anno-value"
ORAS("manifest", "index", "create", Flags.Layout, indexRef, "--annotation", fmt.Sprintf("%s=%s", key, value)).Exec()
// verify
content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents()
var manifest ocispec.Manifest
Expect(json.Unmarshal(content, &manifest)).ShouldNot(HaveOccurred())
Expect(manifest.Annotations[key]).To(Equal(value))
})

It("should output created index to file", func() {
root := PrepareTempOCI(ImageRepo)
indexRef := LayoutRef(root, "output-to-file")
Expand Down
Loading