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: add platform flag to cosign copy command #3234

Merged
merged 8 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
vishal-chdhry marked this conversation as resolved.
Show resolved Hide resolved
}, 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
Loading