From 6b03a64ca8595f72043cbc0464d6a6c3594df0aa Mon Sep 17 00:00:00 2001 From: Magnus Bengtsson Date: Fri, 12 Aug 2022 09:59:55 +0200 Subject: [PATCH 1/5] 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 From eee2abd4659d09034dc01ffba7c1b08127339bf0 Mon Sep 17 00:00:00 2001 From: Magnus Bengtsson Date: Mon, 22 Aug 2022 20:47:36 +0200 Subject: [PATCH 2/5] Fix failing checks Signed-off-by: Magnus Bengtsson --- doc/cosign_upload_blob.md | 7 +++++++ pkg/cosign/remote/index_test.go | 9 ++++----- pkg/cosign/remote/testdata/bar | 2 +- pkg/cosign/remote/testdata/foo | 2 +- test/e2e_test.go | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/doc/cosign_upload_blob.md b/doc/cosign_upload_blob.md index 622603d40a7..067ec210b3d 100644 --- a/doc/cosign_upload_blob.md +++ b/doc/cosign_upload_blob.md @@ -22,12 +22,19 @@ cosign upload blob [flags] # 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 + + # 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 ``` ### Options ``` --allow-insecure-registry whether to allow insecure connections to registries. Don't use this for anything but testing + -a, --annotation stringToString annotations to set (default []) --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --ct string content type to set -f, --files strings :[platform/arch] diff --git a/pkg/cosign/remote/index_test.go b/pkg/cosign/remote/index_test.go index ce41c6a42d3..9f960b40bb9 100644 --- a/pkg/cosign/remote/index_test.go +++ b/pkg/cosign/remote/index_test.go @@ -215,7 +215,6 @@ func TestUploadFiles(t *testing.T) { } }) } - } func checkPlatform(t *testing.T, ref name.Reference, p *v1.Platform) bool { @@ -272,11 +271,12 @@ func checkIndex(t *testing.T, ref name.Reference) bool { func checkAnnotations(t *testing.T, ref name.Reference, annotations map[string]string) bool { t.Helper() - var found bool = false + var found bool 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 { @@ -286,7 +286,7 @@ func checkAnnotations(t *testing.T, ref name.Reference, annotations map[string]s if err != nil { t.Fatalf("IndexManifest() = %v", err) } - for annotationsKey, _ := range annotations { + for annotationsKey := range annotations { _, ok := m.Annotations[annotationsKey] if ok { found = true @@ -301,7 +301,7 @@ func checkAnnotations(t *testing.T, ref name.Reference, annotations map[string]s t.Fatalf("Image() = %v", err) } m, _ := i.Manifest() - for annotationsKey, _ := range annotations { + for annotationsKey := range annotations { _, ok := m.Annotations[annotationsKey] if ok { found = true @@ -309,6 +309,5 @@ func checkAnnotations(t *testing.T, ref name.Reference, annotations map[string]s } return found } - return false } diff --git a/pkg/cosign/remote/testdata/bar b/pkg/cosign/remote/testdata/bar index ba0e162e1c4..5716ca5987c 100644 --- a/pkg/cosign/remote/testdata/bar +++ b/pkg/cosign/remote/testdata/bar @@ -1 +1 @@ -bar \ No newline at end of file +bar diff --git a/pkg/cosign/remote/testdata/foo b/pkg/cosign/remote/testdata/foo index 19102815663..257cc5642cb 100644 --- a/pkg/cosign/remote/testdata/foo +++ b/pkg/cosign/remote/testdata/foo @@ -1 +1 @@ -foo \ No newline at end of file +foo diff --git a/test/e2e_test.go b/test/e2e_test.go index a56948cb341..2a7697df1a0 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -909,7 +909,7 @@ func TestUploadBlob(t *testing.T) { // Upload it! files := []cremote.File{cremote.FileFromFlag(payloadPath)} - must(upload.BlobCmd(ctx, options.RegistryOptions{}, files, "", imgName), t) + must(upload.BlobCmd(ctx, options.RegistryOptions{}, files, nil, "", imgName), t) // Check it ref, err := name.ParseReference(imgName) From f6a105f537ca861f9b0c38a03d7680a47c79c2b8 Mon Sep 17 00:00:00 2001 From: Magnus Bengtsson Date: Tue, 23 Aug 2022 11:40:23 +0200 Subject: [PATCH 3/5] Last linter errors Signed-off-by: Magnus Bengtsson --- cmd/cosign/cli/upload.go | 2 +- pkg/cosign/remote/index_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/cosign/cli/upload.go b/cmd/cosign/cli/upload.go index ad2f52db416..b05150b762a 100644 --- a/cmd/cosign/cli/upload.go +++ b/cmd/cosign/cli/upload.go @@ -57,7 +57,7 @@ func uploadBlob() *cobra.Command { # 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 - + # upload a blob named foo to the location specified by , setting annotations mykey=myvalue. cosign upload blob -a mykey=myvalue -f foo diff --git a/pkg/cosign/remote/index_test.go b/pkg/cosign/remote/index_test.go index 9f960b40bb9..08956b67799 100644 --- a/pkg/cosign/remote/index_test.go +++ b/pkg/cosign/remote/index_test.go @@ -254,7 +254,6 @@ func checkPlatform(t *testing.T, ref name.Reference, p *v1.Platform) bool { } } return false - } func checkIndex(t *testing.T, ref name.Reference) bool { From 5c8b2dde0890fdb5b22dd76b6903ff113f77c760 Mon Sep 17 00:00:00 2001 From: Magnus Bengtsson Date: Wed, 24 Aug 2022 17:45:02 +0200 Subject: [PATCH 4/5] docgen Signed-off-by: Magnus Bengtsson --- doc/cosign_upload_blob.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/cosign_upload_blob.md b/doc/cosign_upload_blob.md index 067ec210b3d..395f13039e5 100644 --- a/doc/cosign_upload_blob.md +++ b/doc/cosign_upload_blob.md @@ -22,7 +22,7 @@ cosign upload blob [flags] # 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 - + # upload a blob named foo to the location specified by , setting annotations mykey=myvalue. cosign upload blob -a mykey=myvalue -f foo From c937cf5caf828398d4498f1e0ac8965a4fc481e1 Mon Sep 17 00:00:00 2001 From: Magnus Bengtsson Date: Tue, 30 Aug 2022 20:22:28 +0200 Subject: [PATCH 5/5] use options instead of casting to a v1.image Signed-off-by: Magnus Bengtsson --- pkg/cosign/remote/index.go | 12 ++---------- pkg/oci/static/file.go | 3 +++ pkg/oci/static/file_test.go | 13 ++++++++++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pkg/cosign/remote/index.go b/pkg/cosign/remote/index.go index 81310ebce08..54871a7f8f9 100644 --- a/pkg/cosign/remote/index.go +++ b/pkg/cosign/remote/index.go @@ -109,7 +109,7 @@ func UploadFiles(ref name.Reference, files []File, annotations map[string]string mt := getMt(b) fmt.Fprintf(os.Stderr, "Uploading file from [%s] to [%s] with media type [%s]\n", f.Path(), ref.Name(), mt) - img, err := static.NewFile(b, static.WithLayerMediaType(mt)) + img, err := static.NewFile(b, static.WithLayerMediaType(mt), static.WithAnnotations(annotations)) if err != nil { return name.Digest{}, err } @@ -119,15 +119,7 @@ func UploadFiles(ref name.Reference, files []File, annotations map[string]string return name.Digest{}, err } - // 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 { + if err := remote.Write(ref, img, remoteOpts...); err != nil { return name.Digest{}, err } l, err := img.Layers() diff --git a/pkg/oci/static/file.go b/pkg/oci/static/file.go index d1d895bdb66..eefde1268a7 100644 --- a/pkg/oci/static/file.go +++ b/pkg/oci/static/file.go @@ -46,6 +46,9 @@ func NewFile(payload []byte, opts ...Option) (oci.File, error) { return nil, err } + // Add annotations from options + img = mutate.Annotations(img, o.Annotations).(v1.Image) + // Set the Created date to time of execution img, err = mutate.CreatedAt(img, v1.Time{Time: time.Now()}) if err != nil { diff --git a/pkg/oci/static/file_test.go b/pkg/oci/static/file_test.go index c652a7a318d..3c2877a4715 100644 --- a/pkg/oci/static/file_test.go +++ b/pkg/oci/static/file_test.go @@ -27,7 +27,7 @@ import ( func TestNewFile(t *testing.T) { payload := "this is the content!" - file, err := NewFile([]byte(payload), WithLayerMediaType("foo")) + file, err := NewFile([]byte(payload), WithLayerMediaType("foo"), WithAnnotations(map[string]string{"foo": "bar"})) if err != nil { t.Fatalf("NewFile() = %v", err) } @@ -130,4 +130,15 @@ func TestNewFile(t *testing.T) { t.Errorf("Date of Signature was Zero") } }) + + t.Run("check annotations", func(t *testing.T) { + m, err := file.Manifest() + if err != nil { + t.Fatalf("Manifest() = %v", err) + } + gotAnnotations := m.Annotations + if got, want := gotAnnotations["foo"], "bar"; got != want { + t.Errorf("Annotations = %s, wanted %s", got, want) + } + }) }