From 028a7308b599e32d05336f4d9ff31f068babc55c Mon Sep 17 00:00:00 2001 From: Magnus Bengtsson Date: Fri, 12 Aug 2022 09:59:55 +0200 Subject: [PATCH] Add annotations for upload blob. Add tests for UploadFile() Signed-off-by: Magnus Bengtsson --- cmd/cosign/cli/options/upload.go | 3 + cmd/cosign/cli/policy_init.go | 4 +- cmd/cosign/cli/upload.go | 10 +- cmd/cosign/cli/upload/blob.go | 4 +- pkg/cosign/remote/index.go | 19 ++- pkg/cosign/remote/index_test.go | 219 +++++++++++++++++++++++++++++++ pkg/cosign/remote/testdata/bar | 1 + pkg/cosign/remote/testdata/foo | 1 + 8 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 pkg/cosign/remote/testdata/bar create mode 100644 pkg/cosign/remote/testdata/foo diff --git a/cmd/cosign/cli/options/upload.go b/cmd/cosign/cli/options/upload.go index 9d7b77ac9d7..c9154ea8f40 100644 --- a/cmd/cosign/cli/options/upload.go +++ b/cmd/cosign/cli/options/upload.go @@ -24,6 +24,7 @@ type UploadBlobOptions struct { ContentType string Files FilesOptions Registry RegistryOptions + Annotations map[string]string } var _ Interface = (*UploadBlobOptions)(nil) @@ -35,6 +36,8 @@ func (o *UploadBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.ContentType, "ct", "", "content type to set") + cmd.Flags().StringToStringVarP(&o.Annotations, "annotation", "a", nil, + "annotations to set") } // UploadWASMOptions is the top level wrapper for the `upload wasm` command. diff --git a/cmd/cosign/cli/policy_init.go b/cmd/cosign/cli/policy_init.go index a754ab9ea76..361fe28fe43 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -150,7 +150,7 @@ func initPolicy() *cobra.Command { cremote.FileFromFlag(outfile), } - return upload.BlobCmd(cmd.Context(), o.Registry, files, "", rootPath(o.ImageRef)) + return upload.BlobCmd(cmd.Context(), o.Registry, files, nil, "", rootPath(o.ImageRef)) }, } @@ -297,7 +297,7 @@ func signPolicy() *cobra.Command { cremote.FileFromFlag(outfile), } - return upload.BlobCmd(ctx, o.Registry, files, "", rootPath(o.ImageRef)) + return upload.BlobCmd(ctx, o.Registry, files, nil, "", rootPath(o.ImageRef)) }, } diff --git a/cmd/cosign/cli/upload.go b/cmd/cosign/cli/upload.go index 3c0c362f856..ad2f52db416 100644 --- a/cmd/cosign/cli/upload.go +++ b/cmd/cosign/cli/upload.go @@ -56,7 +56,13 @@ func uploadBlob() *cobra.Command { cosign upload blob -f foo:MYOS/MYPLATFORM # upload two blobs named foo-darwin and foo-linux to the location specified by , setting the os fields - cosign upload blob -f foo-darwin:darwin -f foo-linux:linux `, + cosign upload blob -f foo-darwin:darwin -f foo-linux:linux + + # upload a blob named foo to the location specified by , setting annotations mykey=myvalue. + cosign upload blob -a mykey=myvalue -f foo + + # upload two blobs named foo-darwin and foo-linux to the location specified by , setting annotations + cosign upload blob -a mykey=myvalue -a myotherkey="my other value" -f foo-darwin:darwin -f foo-linux:linux `, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { if len(o.Files.Files) < 1 { @@ -70,7 +76,7 @@ func uploadBlob() *cobra.Command { return err } - return upload.BlobCmd(cmd.Context(), o.Registry, files, o.ContentType, args[0]) + return upload.BlobCmd(cmd.Context(), o.Registry, files, o.Annotations, o.ContentType, args[0]) }, } diff --git a/cmd/cosign/cli/upload/blob.go b/cmd/cosign/cli/upload/blob.go index 7580ba833c6..87f70bf22bb 100644 --- a/cmd/cosign/cli/upload/blob.go +++ b/cmd/cosign/cli/upload/blob.go @@ -28,7 +28,7 @@ import ( cremote "github.com/sigstore/cosign/pkg/cosign/remote" ) -func BlobCmd(ctx context.Context, regOpts options.RegistryOptions, files []cremote.File, contentType, imageRef string) error { +func BlobCmd(ctx context.Context, regOpts options.RegistryOptions, files []cremote.File, annotations map[string]string, contentType, imageRef string) error { ref, err := name.ParseReference(imageRef) if err != nil { return err @@ -43,7 +43,7 @@ func BlobCmd(ctx context.Context, regOpts options.RegistryOptions, files []cremo } } - dgstAddr, err := cremote.UploadFiles(ref, files, mt, regOpts.GetRegistryClientOpts(ctx)...) + dgstAddr, err := cremote.UploadFiles(ref, files, annotations, mt, regOpts.GetRegistryClientOpts(ctx)...) if err != nil { return err } diff --git a/pkg/cosign/remote/index.go b/pkg/cosign/remote/index.go index d40deb2a867..81310ebce08 100644 --- a/pkg/cosign/remote/index.go +++ b/pkg/cosign/remote/index.go @@ -97,7 +97,7 @@ func DefaultMediaTypeGetter(b []byte) types.MediaType { return types.MediaType(strings.Split(http.DetectContentType(b), ";")[0]) } -func UploadFiles(ref name.Reference, files []File, getMt MediaTypeGetter, remoteOpts ...remote.Option) (name.Digest, error) { +func UploadFiles(ref name.Reference, files []File, annotations map[string]string, getMt MediaTypeGetter, remoteOpts ...remote.Option) (name.Digest, error) { var lastHash v1.Hash var idx v1.ImageIndex = empty.Index @@ -113,11 +113,21 @@ func UploadFiles(ref name.Reference, files []File, getMt MediaTypeGetter, remote if err != nil { return name.Digest{}, err } + lastHash, err = img.Digest() if err != nil { return name.Digest{}, err } - if err := remote.Write(ref, img, remoteOpts...); err != nil { + + // cast img to a v1.image + v1Img, ok := img.(v1.Image) + if !ok { + return name.Digest{}, fmt.Errorf("unable to cast image to v1.Image") + } + if annotations != nil { + v1Img = mutate.Annotations(v1Img, annotations).(v1.Image) + } + if err := remote.Write(ref, v1Img, remoteOpts...); err != nil { return name.Digest{}, err } l, err := img.Layers() @@ -128,8 +138,10 @@ func UploadFiles(ref name.Reference, files []File, getMt MediaTypeGetter, remote if err != nil { return name.Digest{}, err } + blobURL := ref.Context().Registry.RegistryStr() + "/v2/" + ref.Context().RepositoryStr() + "/blobs/" + layerHash.String() fmt.Fprintf(os.Stderr, "File [%s] is available directly at [%s]\n", f.Path(), blobURL) + if f.Platform() != nil { idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ Add: img, @@ -141,6 +153,9 @@ func UploadFiles(ref name.Reference, files []File, getMt MediaTypeGetter, remote } if len(files) > 1 { + if annotations != nil { + idx = mutate.Annotations(idx, annotations).(v1.ImageIndex) + } err := remote.WriteIndex(ref, idx, remoteOpts...) if err != nil { return name.Digest{}, err diff --git a/pkg/cosign/remote/index_test.go b/pkg/cosign/remote/index_test.go index 1e6c4bbf267..ce41c6a42d3 100644 --- a/pkg/cosign/remote/index_test.go +++ b/pkg/cosign/remote/index_test.go @@ -16,10 +16,18 @@ package remote import ( + "fmt" + "io/ioutil" + "log" + "net/http/httptest" + "net/url" "reflect" "testing" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" ) func TestFilesFromFlagList(t *testing.T) { @@ -93,3 +101,214 @@ func TestFileFromFlag(t *testing.T) { }) } } + +func TestUploadFiles(t *testing.T) { + var ( + expectedRepo = "foo" + mt = DefaultMediaTypeGetter + err error + ) + // Set up a fake registry (with NOP logger to avoid spamming test logs). + nopLog := log.New(ioutil.Discard, "", 0) + s := httptest.NewServer(registry.New(registry.Logger(nopLog))) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + image string + fs []File + annotations map[string]string + wantErr bool + }{{ + name: "one file", + image: "one", + fs: []File{ + &file{path: "testdata/foo"}, + }, + wantErr: false, + }, { + name: "missing file", + image: "one-missing", + fs: []File{ + &file{path: "testdata/missing"}, + }, + wantErr: true, + }, + { + name: "two files with platform", + image: "two-platform", + fs: []File{ + &file{path: "testdata/foo", platform: &v1.Platform{ + OS: "darwin", + Architecture: "amd64", + }}, + &file{path: "testdata/bar", platform: &v1.Platform{ + OS: "linux", + Architecture: "amd64", + }}, + }, + wantErr: false, + }, { + name: "one file with annotations", + image: "one-annotations", + fs: []File{ + &file{path: "testdata/foo"}, + }, + annotations: map[string]string{ + "foo": "bar", + }, + wantErr: false, + }, { + name: "two files with annotations", + image: "two-annotations", + fs: []File{ + &file{path: "testdata/foo", platform: &v1.Platform{ + OS: "darwin", + Architecture: "amd64", + }}, + &file{path: "testdata/bar", platform: &v1.Platform{ + OS: "linux", + Architecture: "amd64", + }}, + }, + annotations: map[string]string{ + "foo": "bar", + "bar": "baz", + }, + wantErr: false, + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(fmt.Sprintf("%s/%s/%s:latest", u.Host, expectedRepo, tt.image)) + if err != nil { + t.Fatalf("ParseRef() = %v", err) + } + _, err = UploadFiles(ref, tt.fs, tt.annotations, mt) + + if (err != nil) != tt.wantErr { + t.Errorf("UploadFiles() error = %v, wantErr %v", err, tt.wantErr) + } + + if len(tt.fs) > 1 { + if !checkIndex(t, ref) { + t.Errorf("UploadFiles() index = %v", checkIndex(t, ref)) + } + } + + for _, f := range tt.fs { + if f.Platform() != nil { + if !checkPlatform(t, ref, f.Platform()) { + t.Errorf("UploadFiles() platform = %v was not found", checkPlatform(t, ref, f.Platform())) + } + } + } + + if tt.annotations != nil { + if !checkAnnotations(t, ref, tt.annotations) { + t.Errorf("UploadFiles() annotations = %v was not found", checkAnnotations(t, ref, tt.annotations)) + } + } + }) + } + +} + +func checkPlatform(t *testing.T, ref name.Reference, p *v1.Platform) bool { + t.Helper() + d, err := remote.Get(ref) + if err != nil { + t.Fatalf("Get() = %v", err) + } + + if d.MediaType.IsIndex() { + // if it is an index recurse into the index + idx, err := d.ImageIndex() + if err != nil { + t.Fatalf("ImageIndex() = %v", err) + } + manifest, err := idx.IndexManifest() + if err != nil { + t.Fatalf("IndexManifest() = %v", err) + } + for _, m := range manifest.Manifests { + if !m.MediaType.IsImage() { + return false + } + if m.Platform != nil { + if m.Platform.OS == p.OS && m.Platform.Architecture == p.Architecture { + return true + } + } + } + return false + } else if d.MediaType.IsImage() { + // if it is an image check the platform + if d.Platform != nil { + if d.Platform.OS == p.OS && d.Platform.Architecture == p.Architecture { + return true + } + } + } + return false + +} + +func checkIndex(t *testing.T, ref name.Reference) bool { + t.Helper() + d, err := remote.Get(ref) + if err != nil { + t.Fatalf("Get() = %v", err) + } + if d.MediaType.IsIndex() { + return true + } + return false +} + +func checkAnnotations(t *testing.T, ref name.Reference, annotations map[string]string) bool { + t.Helper() + var found bool = false + d, err := remote.Get(ref) + if err != nil { + t.Fatalf("Get() = %v", err) + } + if d.MediaType.IsIndex() { + idx, err := remote.Index(ref) + if err != nil { + t.Fatalf("Index() = %v", err) + } + m, err := idx.IndexManifest() + if err != nil { + t.Fatalf("IndexManifest() = %v", err) + } + for annotationsKey, _ := range annotations { + _, ok := m.Annotations[annotationsKey] + if ok { + found = true + } + } + return found + } + + if d.MediaType.IsImage() { + i, err := remote.Image(ref) + if err != nil { + t.Fatalf("Image() = %v", err) + } + m, _ := i.Manifest() + for annotationsKey, _ := range annotations { + _, ok := m.Annotations[annotationsKey] + if ok { + found = true + } + } + return found + } + + return false +} diff --git a/pkg/cosign/remote/testdata/bar b/pkg/cosign/remote/testdata/bar new file mode 100644 index 00000000000..ba0e162e1c4 --- /dev/null +++ b/pkg/cosign/remote/testdata/bar @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/pkg/cosign/remote/testdata/foo b/pkg/cosign/remote/testdata/foo new file mode 100644 index 00000000000..19102815663 --- /dev/null +++ b/pkg/cosign/remote/testdata/foo @@ -0,0 +1 @@ +foo \ No newline at end of file