diff --git a/cmd/cosign/cli/copy.go b/cmd/cosign/cli/copy.go index 7c23f7e3b70..5af36483fd1 100644 --- a/cmd/cosign/cli/copy.go +++ b/cmd/cosign/cli/copy.go @@ -37,12 +37,15 @@ func Copy() *cobra.Command { cosign copy --sig-only example.com/src example.com/dest # overwrite destination image and signatures - cosign copy -f example.com/src example.com/dest`, + cosign copy -f example.com/src example.com/dest + + # copy a container image and its signatures for a specific platform + cosign copy --platform=linux/amd64 example.com/src:latest example.com/dest:latest`, Args: cobra.ExactArgs(2), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { - return copy.CopyCmd(cmd.Context(), o.Registry, args[0], args[1], o.SignatureOnly, o.Force) + return copy.CopyCmd(cmd.Context(), o.Registry, args[0], args[1], o.SignatureOnly, o.Force, o.Platform) }, } diff --git a/cmd/cosign/cli/copy/copy.go b/cmd/cosign/cli/copy/copy.go index 0c0d6d2b975..462c57d282d 100644 --- a/cmd/cosign/cli/copy/copy.go +++ b/cmd/cosign/cli/copy/copy.go @@ -28,6 +28,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/pkg/oci" + ociplatform "github.com/sigstore/cosign/v2/pkg/oci/platform" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" "github.com/sigstore/cosign/v2/pkg/oci/walk" "golang.org/x/sync/errgroup" @@ -35,7 +36,7 @@ import ( // CopyCmd implements the logic to copy the supplied container image and signatures. // nolint -func CopyCmd(ctx context.Context, regOpts options.RegistryOptions, srcImg, dstImg string, sigOnly, force bool) error { +func CopyCmd(ctx context.Context, regOpts options.RegistryOptions, srcImg, dstImg string, sigOnly, force bool, platform string) error { no := regOpts.NameOptions() srcRef, err := name.ParseReference(srcImg, no...) if err != nil { @@ -71,6 +72,11 @@ func CopyCmd(ctx context.Context, regOpts options.RegistryOptions, srcImg, dstIm return err } + root, err = ociplatform.SignedEntityForPlatform(root, platform) + if err != nil { + return err + } + if err := walk.SignedEntity(gctx, root, func(ctx context.Context, se oci.SignedEntity) error { // Both of the SignedEntity types implement Digest() h, err := se.Digest() diff --git a/cmd/cosign/cli/copy/copy_test.go b/cmd/cosign/cli/copy/copy_test.go index e1b4671f09c..f89c993bfd2 100644 --- a/cmd/cosign/cli/copy/copy_test.go +++ b/cmd/cosign/cli/copy/copy_test.go @@ -33,8 +33,20 @@ func TestCopyAttachmentTagPrefix(t *testing.T) { err := CopyCmd(ctx, options.RegistryOptions{ RefOpts: refOpts, - }, srcImg, destImg, false, true) + }, srcImg, destImg, false, true, "") if err == nil { t.Fatal("failed to copy with attachment-tag-prefix") } } + +func TestCopyPlatformOpt(t *testing.T) { + ctx := context.Background() + + srcImg := "alpine" + destImg := "test-alpine" + + err := CopyCmd(ctx, options.RegistryOptions{}, srcImg, destImg, false, true, "linux/amd64") + if err == nil { + t.Fatal("failed to copy with platform") + } +} diff --git a/cmd/cosign/cli/download/attestation.go b/cmd/cosign/cli/download/attestation.go index 92a2f6603d9..1861494aadc 100644 --- a/cmd/cosign/cli/download/attestation.go +++ b/cmd/cosign/cli/download/attestation.go @@ -21,10 +21,9 @@ import ( "fmt" "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/pkg/cosign" - "github.com/sigstore/cosign/v2/pkg/oci" + "github.com/sigstore/cosign/v2/pkg/oci/platform" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" ) @@ -51,42 +50,9 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOpt return err } - idx, isIndex := se.(oci.SignedImageIndex) - - // We only allow --platform on multiarch indexes - if attOptions.Platform != "" && !isIndex { - return fmt.Errorf("specified reference is not a multiarch image") - } - - if attOptions.Platform != "" && isIndex { - targetPlatform, err := v1.ParsePlatform(attOptions.Platform) - if err != nil { - return fmt.Errorf("parsing platform: %w", err) - } - platforms, err := getIndexPlatforms(idx) - if err != nil { - return fmt.Errorf("getting available platforms: %w", err) - } - - platforms = matchPlatform(targetPlatform, platforms) - if len(platforms) == 0 { - return fmt.Errorf("unable to find an attestation for %s", targetPlatform.String()) - } - if len(platforms) > 1 { - return fmt.Errorf( - "platform spec matches more than one image architecture: %s", - platforms.String(), - ) - } - - nse, err := idx.SignedImage(platforms[0].hash) - if err != nil { - return fmt.Errorf("searching for %s image: %w", platforms[0].hash.String(), err) - } - if nse == nil { - return fmt.Errorf("unable to find image %s", platforms[0].hash.String()) - } - se = nse + se, err = platform.SignedEntityForPlatform(se, attOptions.Platform) + if err != nil { + return err } attestations, err := cosign.FetchAttestations(se, predicateType) diff --git a/cmd/cosign/cli/download/sbom.go b/cmd/cosign/cli/download/sbom.go index 0d4816e4ca9..2e3c41a1c97 100644 --- a/cmd/cosign/cli/download/sbom.go +++ b/cmd/cosign/cli/download/sbom.go @@ -21,28 +21,14 @@ import ( "fmt" "io" "os" - "strings" "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/pkg/oci" + "github.com/sigstore/cosign/v2/pkg/oci/platform" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" ) -type platformList []struct { - hash v1.Hash - platform *v1.Platform -} - -func (pl *platformList) String() string { - r := []string{} - for _, p := range *pl { - r = append(r, p.platform.String()) - } - return strings.Join(r, ", ") -} - func SBOMCmd( ctx context.Context, regOpts options.RegistryOptions, dnOpts options.SBOMDownloadOptions, imageRef string, out io.Writer, @@ -62,43 +48,12 @@ func SBOMCmd( return nil, err } - idx, isIndex := se.(oci.SignedImageIndex) - - // We only allow --platform on multiarch indexes - if dnOpts.Platform != "" && !isIndex { - return nil, fmt.Errorf("specified reference is not a multiarch image") + se, err = platform.SignedEntityForPlatform(se, dnOpts.Platform) + if err != nil { + return nil, err } - if dnOpts.Platform != "" && isIndex { - targetPlatform, err := v1.ParsePlatform(dnOpts.Platform) - if err != nil { - return nil, fmt.Errorf("parsing platform: %w", err) - } - platforms, err := getIndexPlatforms(idx) - if err != nil { - return nil, fmt.Errorf("getting available platforms: %w", err) - } - - platforms = matchPlatform(targetPlatform, platforms) - if len(platforms) == 0 { - return nil, fmt.Errorf("unable to find an SBOM for %s", targetPlatform.String()) - } - if len(platforms) > 1 { - return nil, fmt.Errorf( - "platform spec matches more than one image architecture: %s", - platforms.String(), - ) - } - - nse, err := idx.SignedImage(platforms[0].hash) - if err != nil { - return nil, fmt.Errorf("searching for %s image: %w", platforms[0].hash.String(), err) - } - if nse == nil { - return nil, fmt.Errorf("unable to find image %s", platforms[0].hash.String()) - } - se = nse - } + idx, isIndex := se.(oci.SignedImageIndex) file, err := se.Attachment("sbom") if errors.Is(err, ociremote.ErrImageNotFound) { @@ -106,7 +61,7 @@ func SBOMCmd( return nil, errors.New("no sbom attached to reference") } // Help the user with the available architectures - pl, err := getIndexPlatforms(idx) + pl, err := platform.GetIndexPlatforms(idx) if len(pl) > 0 && err == nil { fmt.Fprintf( os.Stderr, @@ -139,57 +94,3 @@ func SBOMCmd( return sboms, nil } - -func getIndexPlatforms(idx oci.SignedImageIndex) (platformList, error) { - im, err := idx.IndexManifest() - if err != nil { - return nil, fmt.Errorf("fetching index manifest: %w", err) - } - - platforms := platformList{} - for _, m := range im.Manifests { - if m.Platform == nil { - continue - } - platforms = append(platforms, struct { - hash v1.Hash - platform *v1.Platform - }{m.Digest, m.Platform}) - } - return platforms, nil -} - -// matchPlatform filters a list of platforms returning only those matching -// a base. "Based" on ko's internal equivalent while it moves to GGCR. -// https://github.com/google/ko/blob/e6a7a37e26d82a8b2bb6df991c5a6cf6b2728794/pkg/build/gobuild.go#L1020 -func matchPlatform(base *v1.Platform, list platformList) platformList { - ret := platformList{} - for _, p := range list { - if base.OS != "" && base.OS != p.platform.OS { - continue - } - if base.Architecture != "" && base.Architecture != p.platform.Architecture { - continue - } - if base.Variant != "" && base.Variant != p.platform.Variant { - continue - } - - if base.OSVersion != "" && p.platform.OSVersion != base.OSVersion { - if base.OS != "windows" { - continue - } else { //nolint: revive - if pcount, bcount := strings.Count(base.OSVersion, "."), strings.Count(p.platform.OSVersion, "."); pcount == 2 && bcount == 3 { - if base.OSVersion != p.platform.OSVersion[:strings.LastIndex(p.platform.OSVersion, ".")] { - continue - } - } else { - continue - } - } - } - ret = append(ret, p) - } - - return ret -} diff --git a/cmd/cosign/cli/options/copy.go b/cmd/cosign/cli/options/copy.go index e9c5c70134b..4e2a99125f9 100644 --- a/cmd/cosign/cli/options/copy.go +++ b/cmd/cosign/cli/options/copy.go @@ -23,6 +23,7 @@ import ( type CopyOptions struct { SignatureOnly bool Force bool + Platform string Registry RegistryOptions } @@ -37,4 +38,7 @@ func (o *CopyOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVarP(&o.Force, "force", "f", false, "overwrite destination image(s), if necessary") + + cmd.Flags().StringVar(&o.Platform, "platform", "", + "only copy container image and its signatures for a specific platform image") } diff --git a/doc/cosign_copy.md b/doc/cosign_copy.md index 37a4fb0cdf1..331d292516e 100644 --- a/doc/cosign_copy.md +++ b/doc/cosign_copy.md @@ -19,6 +19,9 @@ cosign copy [flags] # overwrite destination image and signatures cosign copy -f example.com/src example.com/dest + + # copy a container image and its signatures for a specific platform + cosign copy --platform=linux/amd64 example.com/src:latest example.com/dest:latest ``` ### Options @@ -30,6 +33,7 @@ cosign copy [flags] -f, --force overwrite destination image(s), if necessary -h, --help help for copy --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). + --platform string only copy container image and its signatures for a specific platform image --sig-only only copy the image signature ``` diff --git a/pkg/oci/platform/platform.go b/pkg/oci/platform/platform.go new file mode 100644 index 00000000000..a2939d73660 --- /dev/null +++ b/pkg/oci/platform/platform.go @@ -0,0 +1,133 @@ +// Copyright 2023 the Sigstore 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 platform + +import ( + "fmt" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/sigstore/cosign/v2/pkg/oci" +) + +type List []struct { + Hash v1.Hash + Platform *v1.Platform +} + +func (pl *List) String() string { + r := []string{} + for _, p := range *pl { + r = append(r, p.Platform.String()) + } + return strings.Join(r, ", ") +} + +func GetIndexPlatforms(idx oci.SignedImageIndex) (List, error) { + im, err := idx.IndexManifest() + if err != nil { + return nil, fmt.Errorf("fetching index manifest: %w", err) + } + + platforms := List{} + for _, m := range im.Manifests { + if m.Platform == nil { + continue + } + platforms = append(platforms, struct { + Hash v1.Hash + Platform *v1.Platform + }{m.Digest, m.Platform}) + } + return platforms, nil +} + +// matchPlatform filters a list of platforms returning only those matching +// a base. "Based" on ko's internal equivalent while it moves to GGCR. +// https://github.com/google/ko/blob/e6a7a37e26d82a8b2bb6df991c5a6cf6b2728794/pkg/build/gobuild.go#L1020 +func matchPlatform(base *v1.Platform, list List) List { + ret := List{} + for _, p := range list { + if base.OS != "" && base.OS != p.Platform.OS { + continue + } + if base.Architecture != "" && base.Architecture != p.Platform.Architecture { + continue + } + if base.Variant != "" && base.Variant != p.Platform.Variant { + continue + } + + if base.OSVersion != "" && p.Platform.OSVersion != base.OSVersion { + if base.OS != "windows" { + continue + } else { //nolint: revive + if pcount, bcount := strings.Count(base.OSVersion, "."), strings.Count(p.Platform.OSVersion, "."); pcount == 2 && bcount == 3 { + if base.OSVersion != p.Platform.OSVersion[:strings.LastIndex(p.Platform.OSVersion, ".")] { + continue + } + } else { + continue + } + } + } + ret = append(ret, p) + } + + return ret +} + +func SignedEntityForPlatform(se oci.SignedEntity, platform string) (oci.SignedEntity, error) { + if platform == "" { + // Copy all platforms + return se, nil + } + idx, isIndex := se.(oci.SignedImageIndex) + + // We only allow --platform on multiarch indexes + if !isIndex { + return nil, fmt.Errorf("specified reference is not a multiarch image") + } + + targetPlatform, err := v1.ParsePlatform(platform) + if err != nil { + return nil, fmt.Errorf("parsing platform: %w", err) + } + platforms, err := GetIndexPlatforms(idx) + if err != nil { + return nil, fmt.Errorf("getting available platforms: %w", err) + } + + platforms = matchPlatform(targetPlatform, platforms) + if len(platforms) == 0 { + return nil, fmt.Errorf("unable to find an entity for %s", targetPlatform.String()) + } + if len(platforms) > 1 { + return nil, fmt.Errorf( + "platform spec matches more than one image architecture: %s", + platforms.String(), + ) + } + + nse, err := idx.SignedImage(platforms[0].Hash) + if err != nil { + return nil, fmt.Errorf("searching for %s image: %w", platforms[0].Hash.String(), err) + } + if nse == nil { + return nil, fmt.Errorf("unable to find image %s", platforms[0].Hash.String()) + } + + return nse, nil +}