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

Allow cosign save to reuse save directory to help dedupe shared layers. #3239

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
37 changes: 27 additions & 10 deletions cmd/cosign/cli/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,22 @@ func Load() *cobra.Command {
o := &options.LoadOptions{}

cmd := &cobra.Command{
Use: "load",
Short: "Load a signed image on disk to a remote registry",
Long: "Load a signed image on disk to a remote registry",
Example: ` cosign load --dir <path to directory> <IMAGE>`,
Args: cobra.ExactArgs(1),
Use: "load",
Short: "Load a signed image on disk to a remote registry",
Long: "Load a signed image on disk to a remote registry",
Example: ` cosign load --dir <path to directory> <IMAGE> OR cosign load --dir <path to directory> --registry <REGISTRY>`,
//Args: cobra.ExactArgs(1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we should probably delete this

PersistentPreRun: options.BindViper,
RunE: func(cmd *cobra.Command, args []string) error {
if o.Registry.Name != "" && len(args) != 0 {
return fmt.Errorf("both --registry and image argument provided, only one should be used")
}
if o.Registry.Name != "" && len(args) == 0 {
return LoadCmd(cmd.Context(), *o, "")
}
if len(args) != 1 {
return fmt.Errorf("image argument is required")
}
return LoadCmd(cmd.Context(), *o, args[0])
},
}
Expand All @@ -48,12 +57,17 @@ func Load() *cobra.Command {
}

func LoadCmd(ctx context.Context, opts options.LoadOptions, imageRef string) error {
ref, err := name.ParseReference(imageRef)
if err != nil {
return fmt.Errorf("parsing image name %s: %w", imageRef, err)
var ref name.Reference
var err error
if opts.Registry.Name == "" {
// Use the provided image reference
ref, err = name.ParseReference(imageRef)
if err != nil {
return fmt.Errorf("parsing image name %s: %w", imageRef, err)
}
}

// get the signed image from disk
// get the signed image(s) from disk
sii, err := layout.SignedImageIndex(opts.Directory)
if err != nil {
return fmt.Errorf("signed image index: %w", err)
Expand All @@ -64,5 +78,8 @@ func LoadCmd(ctx context.Context, opts options.LoadOptions, imageRef string) err
return err
}

return remote.WriteSignedImageIndexImages(ref, sii, ociremoteOpts...)
if opts.Registry.Name == "" {
return remote.WriteSignedImageIndexImages(ref, sii, ociremoteOpts...)
}
return remote.WriteSignedImageIndexImagesBulk(opts.Registry.Name, sii, ociremoteOpts...)
}
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ func (o *LoadOptions) AddFlags(cmd *cobra.Command) {
"path to directory where the signed image is stored on disk")
_ = cmd.Flags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
_ = cmd.MarkFlagRequired("dir")

cmd.Flags().StringVar(&o.Registry.Name, "registry", "",
"registry to use for bulk load")
_ = cmd.Flags().SetAnnotation("registry", cobra.BashCompSubdirsInDir, []string{})
}
1 change: 1 addition & 0 deletions cmd/cosign/cli/options/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type RegistryOptions struct {
AllowInsecure bool
AllowHTTPRegistry bool
KubernetesKeychain bool
Name string
RefOpts ReferenceOptions
Keychain Keychain

Expand Down
4 changes: 2 additions & 2 deletions cmd/cosign/cli/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ func SaveCmd(_ context.Context, opts options.SaveOptions, imageRef string) error
if err != nil {
return fmt.Errorf("getting signed image: %w", err)
}
return layout.WriteSignedImage(opts.Directory, si)
return layout.WriteSignedImage(opts.Directory, si, ref)
}

if _, ok := se.(oci.SignedImageIndex); ok {
sii, err := ociremote.SignedImageIndex(ref)
if err != nil {
return fmt.Errorf("getting signed image index: %w", err)
}
return layout.WriteSignedImageIndex(opts.Directory, sii)
return layout.WriteSignedImageIndex(opts.Directory, sii, ref)
}
return errors.New("unknown signed entity")
}
3 changes: 2 additions & 1 deletion doc/cosign_load.md

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

23 changes: 12 additions & 11 deletions pkg/oci/layout/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ import (
)

const (
kindAnnotation = "kind"
imageAnnotation = "dev.cosignproject.cosign/image"
imageIndexAnnotation = "dev.cosignproject.cosign/imageIndex"
sigsAnnotation = "dev.cosignproject.cosign/sigs"
attsAnnotation = "dev.cosignproject.cosign/atts"
KindAnnotation = "kind"
ImageAnnotation = "dev.cosignproject.cosign/image"
ImageRefAnnotation = "org.opencontainers.image.ref.name"
ImageIndexAnnotation = "dev.cosignproject.cosign/imageIndex"
SigsAnnotation = "dev.cosignproject.cosign/sigs"
AttsAnnotation = "dev.cosignproject.cosign/atts"
)

// SignedImageIndex provides access to a local index reference, and its signatures.
Expand Down Expand Up @@ -59,7 +60,7 @@ var _ oci.SignedImageIndex = (*index)(nil)

// Signatures implements oci.SignedImageIndex
func (i *index) Signatures() (oci.Signatures, error) {
img, err := i.imageByAnnotation(sigsAnnotation)
img, err := i.imageByAnnotation(SigsAnnotation)
if err != nil {
return nil, err
}
Expand All @@ -71,7 +72,7 @@ func (i *index) Signatures() (oci.Signatures, error) {

// Attestations implements oci.SignedImageIndex
func (i *index) Attestations() (oci.Signatures, error) {
img, err := i.imageByAnnotation(attsAnnotation)
img, err := i.imageByAnnotation(AttsAnnotation)
if err != nil {
return nil, err
}
Expand All @@ -92,7 +93,7 @@ func (i *index) SignedImage(h v1.Hash) (oci.SignedImage, error) {
var img v1.Image
var err error
if h.String() == ":" {
img, err = i.imageByAnnotation(imageAnnotation)
img, err = i.imageByAnnotation(ImageAnnotation)
} else {
img, err = i.Image(h)
}
Expand All @@ -113,7 +114,7 @@ func (i *index) imageByAnnotation(annotation string) (v1.Image, error) {
return nil, err
}
for _, m := range manifest.Manifests {
if val, ok := m.Annotations[kindAnnotation]; ok && val == annotation {
if val, ok := m.Annotations[KindAnnotation]; ok && val == annotation {
return i.Image(m.Digest)
}
}
Expand All @@ -126,7 +127,7 @@ func (i *index) imageIndexByAnnotation(annotation string) (v1.ImageIndex, error)
return nil, err
}
for _, m := range manifest.Manifests {
if val, ok := m.Annotations[kindAnnotation]; ok && val == annotation {
if val, ok := m.Annotations[KindAnnotation]; ok && val == annotation {
return i.ImageIndex(m.Digest)
}
}
Expand All @@ -138,7 +139,7 @@ func (i *index) SignedImageIndex(h v1.Hash) (oci.SignedImageIndex, error) {
var ii v1.ImageIndex
var err error
if h.String() == ":" {
ii, err = i.imageIndexByAnnotation(imageIndexAnnotation)
ii, err = i.imageIndexByAnnotation(ImageIndexAnnotation)
} else {
ii, err = i.ImageIndex(h)
}
Expand Down
62 changes: 46 additions & 16 deletions pkg/oci/layout/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,69 @@
package layout

import (
"errors"
"fmt"
"os"
"strings"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/sigstore/cosign/v2/pkg/oci"
)

// WriteSignedImage writes the image and all related signatures, attestations and attachments
func WriteSignedImage(path string, si oci.SignedImage) error {
// First, write an empty index
layoutPath, err := layout.Write(path, empty.Index)
func WriteSignedImage(path string, si oci.SignedImage, ref name.Reference) error {
layoutPath, err := layout.FromPath(path)
if os.IsNotExist(err) {
// If the layout doesn't exist, create a new one
layoutPath, err = layout.Write(path, empty.Index)
}
if err != nil {
return err
}

// write the image
if err := appendImage(layoutPath, si, imageAnnotation); err != nil {
if err := appendImage(layoutPath, si, ref, ImageAnnotation); err != nil {
return fmt.Errorf("appending signed image: %w", err)
}
return writeSignedEntity(layoutPath, si)
return writeSignedEntity(layoutPath, si, ref)
}

// WriteSignedImageIndex writes the image index and all related signatures, attestations and attachments
func WriteSignedImageIndex(path string, si oci.SignedImageIndex) error {
// First, write an empty index
layoutPath, err := layout.Write(path, empty.Index)
func WriteSignedImageIndex(path string, si oci.SignedImageIndex, ref name.Reference) error {
layoutPath, err := layout.FromPath(path)
if os.IsNotExist(err) {
// If the layout doesn't exist, create a new one
layoutPath, err = layout.Write(path, empty.Index)
}
if err != nil {
return err
}
// write the image index

// Append the image index
imageRef, err := getImageRef(ref)
if err != nil {
return err // Return the error from getImageRef immediately.
}
if err := layoutPath.AppendIndex(si, layout.WithAnnotations(
map[string]string{kindAnnotation: imageIndexAnnotation},
map[string]string{KindAnnotation: ImageIndexAnnotation, ImageRefAnnotation: imageRef},
)); err != nil {
return fmt.Errorf("appending signed image index: %w", err)
}
return writeSignedEntity(layoutPath, si)

return writeSignedEntity(layoutPath, si, ref)
}

func writeSignedEntity(path layout.Path, se oci.SignedEntity) error {
func writeSignedEntity(path layout.Path, se oci.SignedEntity, ref name.Reference) error {
// write the signatures
sigs, err := se.Signatures()
if err != nil {
return fmt.Errorf("getting signatures: %w", err)
}
if !isEmpty(sigs) {
if err := appendImage(path, sigs, sigsAnnotation); err != nil {
if err := appendImage(path, sigs, ref, SigsAnnotation); err != nil {
return fmt.Errorf("appending signatures: %w", err)
}
}
Expand All @@ -72,7 +89,7 @@ func writeSignedEntity(path layout.Path, se oci.SignedEntity) error {
return fmt.Errorf("getting atts")
}
if !isEmpty(atts) {
if err := appendImage(path, atts, attsAnnotation); err != nil {
if err := appendImage(path, atts, ref, AttsAnnotation); err != nil {
return fmt.Errorf("appending atts: %w", err)
}
}
Expand All @@ -86,8 +103,21 @@ func isEmpty(s oci.Signatures) bool {
return ss == nil
}

func appendImage(path layout.Path, img v1.Image, annotation string) error {
func appendImage(path layout.Path, img v1.Image, ref name.Reference, annotation string) error {
imageRef, err := getImageRef(ref)
if err != nil {
return err // Return the error from getImageRef immediately.
}
return path.AppendImage(img, layout.WithAnnotations(
map[string]string{kindAnnotation: annotation},
map[string]string{KindAnnotation: annotation, ImageRefAnnotation: imageRef},
))
}

func getImageRef(ref name.Reference) (string, error) {
if ref == nil {
return "", errors.New("reference is nil")
}
registry := ref.Context().RegistryStr() + "/"
imageRef := strings.TrimPrefix(ref.Name(), registry)
return imageRef, nil
}
7 changes: 6 additions & 1 deletion pkg/oci/layout/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/sigstore/cosign/v2/pkg/oci"
Expand All @@ -36,7 +37,11 @@ func TestReadWrite(t *testing.T) {
// write random signed image to disk
si := randomSignedImage(t)
tmp := t.TempDir()
if err := WriteSignedImage(tmp, si); err != nil {
ref, err := name.ParseReference("test.com/test")
if err != nil {
t.Fatal(err)
}
if err := WriteSignedImage(tmp, si, ref); err != nil {
t.Fatal(err)
}

Expand Down
Loading