Skip to content

Commit

Permalink
feat: add platform flag to cosign copy command (#3234)
Browse files Browse the repository at this point in the history
* feat: add platform flag to copy

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

* feat: extract platformList to common package

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

* add platform check to copycmd

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

* refactor: add platform package in pkg/oci

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

* feat: add a unit test

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

* feat: add boilerplate

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

* feat: export platform list type

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

* lint: fix linting errors

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>

---------

Signed-off-by: Vishal Choudhary <sendtovishalchoudhary@gmail.com>
  • Loading branch information
vishal-chdhry authored Sep 16, 2023
1 parent ee66f35 commit 80def23
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 147 deletions.
7 changes: 5 additions & 2 deletions cmd/cosign/cli/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}

Expand Down
8 changes: 7 additions & 1 deletion cmd/cosign/cli/copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ 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"
)

// 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 {
Expand Down Expand Up @@ -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()
Expand Down
14 changes: 13 additions & 1 deletion cmd/cosign/cli/copy/copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
42 changes: 4 additions & 38 deletions cmd/cosign/cli/download/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
Expand Down
111 changes: 6 additions & 105 deletions cmd/cosign/cli/download/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -62,51 +48,20 @@ 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) {
if !isIndex {
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,
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
type CopyOptions struct {
SignatureOnly bool
Force bool
Platform string
Registry RegistryOptions
}

Expand All @@ -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")
}
4 changes: 4 additions & 0 deletions doc/cosign_copy.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 80def23

Please sign in to comment.