mirror of
https://github.com/containers/skopeo.git
synced 2025-08-30 03:52:47 +00:00
Merge pull request #2648 from mtrmac/shared-signing
Consolidate options shared between copy and sync to sharedCopyOptions
This commit is contained in:
commit
b395b2b923
@ -12,9 +12,6 @@ import (
|
|||||||
"github.com/containers/image/v5/copy"
|
"github.com/containers/image/v5/copy"
|
||||||
"github.com/containers/image/v5/docker/reference"
|
"github.com/containers/image/v5/docker/reference"
|
||||||
"github.com/containers/image/v5/manifest"
|
"github.com/containers/image/v5/manifest"
|
||||||
"github.com/containers/image/v5/pkg/cli"
|
|
||||||
"github.com/containers/image/v5/pkg/cli/sigstore"
|
|
||||||
"github.com/containers/image/v5/signature/signer"
|
|
||||||
"github.com/containers/image/v5/transports"
|
"github.com/containers/image/v5/transports"
|
||||||
"github.com/containers/image/v5/transports/alltransports"
|
"github.com/containers/image/v5/transports/alltransports"
|
||||||
encconfig "github.com/containers/ocicrypt/config"
|
encconfig "github.com/containers/ocicrypt/config"
|
||||||
@ -23,28 +20,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type copyOptions struct {
|
type copyOptions struct {
|
||||||
global *globalOptions
|
global *globalOptions
|
||||||
deprecatedTLSVerify *deprecatedTLSVerifyOption
|
deprecatedTLSVerify *deprecatedTLSVerifyOption
|
||||||
srcImage *imageOptions
|
srcImage *imageOptions
|
||||||
destImage *imageDestOptions
|
destImage *imageDestOptions
|
||||||
retryOpts *retry.Options
|
retryOpts *retry.Options
|
||||||
additionalTags []string // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these
|
copy *sharedCopyOptions
|
||||||
removeSignatures bool // Do not copy signatures from the source image
|
additionalTags []string // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these
|
||||||
signByFingerprint string // Sign the image using a GPG key with the specified fingerprint
|
signIdentity string // Identity of the signed image, must be a fully specified docker reference
|
||||||
signBySigstoreParamFile string // Sign the image using a sigstore signature per configuration in a param file
|
digestFile string // Write digest to this file
|
||||||
signBySigstorePrivateKey string // Sign the image using a sigstore private key
|
quiet bool // Suppress output information when copying images
|
||||||
signPassphraseFile string // Path pointing to a passphrase file when signing (for either signature format, but only one of them)
|
all bool // Copy all of the images if the source is a list
|
||||||
signIdentity string // Identity of the signed image, must be a fully specified docker reference
|
multiArch commonFlag.OptionalString // How to handle multi architecture images
|
||||||
digestFile string // Write digest to this file
|
encryptLayer []int // The list of layers to encrypt
|
||||||
format commonFlag.OptionalString // Force conversion of the image to a specified format
|
encryptionKeys []string // Keys needed to encrypt the image
|
||||||
quiet bool // Suppress output information when copying images
|
decryptionKeys []string // Keys needed to decrypt the image
|
||||||
all bool // Copy all of the images if the source is a list
|
imageParallelCopies uint // Maximum number of parallel requests when copying images
|
||||||
multiArch commonFlag.OptionalString // How to handle multi architecture images
|
|
||||||
preserveDigests bool // Preserve digests during copy
|
|
||||||
encryptLayer []int // The list of layers to encrypt
|
|
||||||
encryptionKeys []string // Keys needed to encrypt the image
|
|
||||||
decryptionKeys []string // Keys needed to decrypt the image
|
|
||||||
imageParallelCopies uint // Maximum number of parallel requests when copying images
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyCmd(global *globalOptions) *cobra.Command {
|
func copyCmd(global *globalOptions) *cobra.Command {
|
||||||
@ -53,11 +44,13 @@ func copyCmd(global *globalOptions) *cobra.Command {
|
|||||||
srcFlags, srcOpts := imageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "src-", "screds")
|
srcFlags, srcOpts := imageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "src-", "screds")
|
||||||
destFlags, destOpts := imageDestFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "dest-", "dcreds")
|
destFlags, destOpts := imageDestFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "dest-", "dcreds")
|
||||||
retryFlags, retryOpts := retryFlags()
|
retryFlags, retryOpts := retryFlags()
|
||||||
|
copyFlags, copyOpts := sharedCopyFlags()
|
||||||
opts := copyOptions{global: global,
|
opts := copyOptions{global: global,
|
||||||
deprecatedTLSVerify: deprecatedTLSVerifyOpt,
|
deprecatedTLSVerify: deprecatedTLSVerifyOpt,
|
||||||
srcImage: srcOpts,
|
srcImage: srcOpts,
|
||||||
destImage: destOpts,
|
destImage: destOpts,
|
||||||
retryOpts: retryOpts,
|
retryOpts: retryOpts,
|
||||||
|
copy: copyOpts,
|
||||||
}
|
}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "copy [command options] SOURCE-IMAGE DESTINATION-IMAGE",
|
Use: "copy [command options] SOURCE-IMAGE DESTINATION-IMAGE",
|
||||||
@ -80,19 +73,13 @@ See skopeo(1) section "IMAGE NAMES" for the expected format
|
|||||||
flags.AddFlagSet(&srcFlags)
|
flags.AddFlagSet(&srcFlags)
|
||||||
flags.AddFlagSet(&destFlags)
|
flags.AddFlagSet(&destFlags)
|
||||||
flags.AddFlagSet(&retryFlags)
|
flags.AddFlagSet(&retryFlags)
|
||||||
|
flags.AddFlagSet(©Flags)
|
||||||
flags.StringSliceVar(&opts.additionalTags, "additional-tag", []string{}, "additional tags (supports docker-archive)")
|
flags.StringSliceVar(&opts.additionalTags, "additional-tag", []string{}, "additional tags (supports docker-archive)")
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress output information when copying images")
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress output information when copying images")
|
||||||
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
|
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
|
||||||
flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`)
|
flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`)
|
||||||
flags.BoolVar(&opts.preserveDigests, "preserve-digests", false, "Preserve digests of images and lists")
|
|
||||||
flags.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from SOURCE-IMAGE")
|
|
||||||
flags.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`")
|
|
||||||
flags.StringVar(&opts.signBySigstoreParamFile, "sign-by-sigstore", "", "Sign the image using a sigstore parameter file at `PATH`")
|
|
||||||
flags.StringVar(&opts.signBySigstorePrivateKey, "sign-by-sigstore-private-key", "", "Sign the image using a sigstore private key at `PATH`")
|
|
||||||
flags.StringVar(&opts.signPassphraseFile, "sign-passphrase-file", "", "Read a passphrase for signing an image from `PATH`")
|
|
||||||
flags.StringVar(&opts.signIdentity, "sign-identity", "", "Identity of signed image, must be a fully specified docker reference. Defaults to the target docker reference.")
|
flags.StringVar(&opts.signIdentity, "sign-identity", "", "Identity of signed image, must be a fully specified docker reference. Defaults to the target docker reference.")
|
||||||
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file")
|
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file")
|
||||||
flags.VarP(commonFlag.NewOptionalStringValue(&opts.format), "format", "f", `MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifest type of source, with fallbacks)`)
|
|
||||||
flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", []string{}, "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)")
|
flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", []string{}, "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)")
|
||||||
flags.IntSliceVar(&opts.encryptLayer, "encrypt-layer", []int{}, "*Experimental* the 0-indexed layer indices, with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer)")
|
flags.IntSliceVar(&opts.encryptLayer, "encrypt-layer", []int{}, "*Experimental* the 0-indexed layer indices, with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer)")
|
||||||
flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", []string{}, "*Experimental* key needed to decrypt the image")
|
flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", []string{}, "*Experimental* key needed to decrypt the image")
|
||||||
@ -160,14 +147,6 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestType string
|
|
||||||
if opts.format.Present() {
|
|
||||||
manifestType, err = parseManifestFormat(opts.format.Value())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range opts.additionalTags {
|
for _, image := range opts.additionalTags {
|
||||||
ref, err := reference.ParseNormalizedNamed(image)
|
ref, err := reference.ParseNormalizedNamed(image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -237,43 +216,6 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
|
|||||||
decConfig = cc.DecryptConfig
|
decConfig = cc.DecryptConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// c/image/copy.Image does allow creating both simple signing and sigstore signatures simultaneously,
|
|
||||||
// with independent passphrases, but that would make the CLI probably too confusing.
|
|
||||||
// For now, use the passphrase with either, but only one of them.
|
|
||||||
if opts.signPassphraseFile != "" && opts.signByFingerprint != "" && opts.signBySigstorePrivateKey != "" {
|
|
||||||
return fmt.Errorf("Only one of --sign-by and sign-by-sigstore-private-key can be used with sign-passphrase-file")
|
|
||||||
}
|
|
||||||
var passphrase string
|
|
||||||
if opts.signPassphraseFile != "" {
|
|
||||||
p, err := cli.ReadPassphraseFile(opts.signPassphraseFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
passphrase = p
|
|
||||||
} else if opts.signBySigstorePrivateKey != "" {
|
|
||||||
p, err := promptForPassphrase(opts.signBySigstorePrivateKey, os.Stdin, os.Stdout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
passphrase = p
|
|
||||||
} // opts.signByFingerprint triggers a GPG-agent passphrase prompt, possibly using a more secure channel, so we usually shouldn’t prompt ourselves if no passphrase was explicitly provided.
|
|
||||||
|
|
||||||
var signers []*signer.Signer
|
|
||||||
if opts.signBySigstoreParamFile != "" {
|
|
||||||
signer, err := sigstore.NewSignerFromParameterFile(opts.signBySigstoreParamFile, &sigstore.Options{
|
|
||||||
PrivateKeyPassphrasePrompt: func(keyFile string) (string, error) {
|
|
||||||
return promptForPassphrase(keyFile, os.Stdin, os.Stdout)
|
|
||||||
},
|
|
||||||
Stdin: os.Stdin,
|
|
||||||
Stdout: stdout,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error using --sign-by-sigstore: %w", err)
|
|
||||||
}
|
|
||||||
defer signer.Close()
|
|
||||||
signers = append(signers, signer)
|
|
||||||
}
|
|
||||||
|
|
||||||
var signIdentity reference.Named = nil
|
var signIdentity reference.Named = nil
|
||||||
if opts.signIdentity != "" {
|
if opts.signIdentity != "" {
|
||||||
signIdentity, err = reference.ParseNamed(opts.signIdentity)
|
signIdentity, err = reference.ParseNamed(opts.signIdentity)
|
||||||
@ -284,26 +226,22 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
|
|||||||
|
|
||||||
opts.destImage.warnAboutIneffectiveOptions(destRef.Transport())
|
opts.destImage.warnAboutIneffectiveOptions(destRef.Transport())
|
||||||
|
|
||||||
|
copyOpts, cleanupOptions, err := opts.copy.copyOptions(stdout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanupOptions()
|
||||||
|
copyOpts.SignIdentity = signIdentity
|
||||||
|
copyOpts.SourceCtx = sourceCtx
|
||||||
|
copyOpts.DestinationCtx = destinationCtx
|
||||||
|
copyOpts.ImageListSelection = imageListSelection
|
||||||
|
copyOpts.OciDecryptConfig = decConfig
|
||||||
|
copyOpts.OciEncryptLayers = encLayers
|
||||||
|
copyOpts.OciEncryptConfig = encConfig
|
||||||
|
copyOpts.MaxParallelDownloads = opts.imageParallelCopies
|
||||||
|
|
||||||
return retry.IfNecessary(ctx, func() error {
|
return retry.IfNecessary(ctx, func() error {
|
||||||
manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{
|
manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, copyOpts)
|
||||||
RemoveSignatures: opts.removeSignatures,
|
|
||||||
Signers: signers,
|
|
||||||
SignBy: opts.signByFingerprint,
|
|
||||||
SignPassphrase: passphrase,
|
|
||||||
SignBySigstorePrivateKeyFile: opts.signBySigstorePrivateKey,
|
|
||||||
SignSigstorePrivateKeyPassphrase: []byte(passphrase),
|
|
||||||
SignIdentity: signIdentity,
|
|
||||||
ReportWriter: stdout,
|
|
||||||
SourceCtx: sourceCtx,
|
|
||||||
DestinationCtx: destinationCtx,
|
|
||||||
ForceManifestMIMEType: manifestType,
|
|
||||||
ImageListSelection: imageListSelection,
|
|
||||||
PreserveDigests: opts.preserveDigests,
|
|
||||||
OciDecryptConfig: decConfig,
|
|
||||||
OciEncryptLayers: encLayers,
|
|
||||||
OciEncryptConfig: encConfig,
|
|
||||||
MaxParallelDownloads: opts.imageParallelCopies,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -57,13 +57,13 @@ skopeo inspect --format "Name: {{.Name}} Digest: {{.Digest}}" docker://registry.
|
|||||||
}
|
}
|
||||||
adjustUsage(cmd)
|
adjustUsage(cmd)
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
|
flags.AddFlagSet(&sharedFlags)
|
||||||
|
flags.AddFlagSet(&imageFlags)
|
||||||
|
flags.AddFlagSet(&retryFlags)
|
||||||
flags.BoolVar(&opts.raw, "raw", false, "output raw manifest or configuration")
|
flags.BoolVar(&opts.raw, "raw", false, "output raw manifest or configuration")
|
||||||
flags.BoolVar(&opts.config, "config", false, "output configuration")
|
flags.BoolVar(&opts.config, "config", false, "output configuration")
|
||||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output to a Go template")
|
flags.StringVarP(&opts.format, "format", "f", "", "Format the output to a Go template")
|
||||||
flags.BoolVarP(&opts.doNotListTags, "no-tags", "n", false, "Do not list the available tags from the repository in the output")
|
flags.BoolVarP(&opts.doNotListTags, "no-tags", "n", false, "Do not list the available tags from the repository in the output")
|
||||||
flags.AddFlagSet(&sharedFlags)
|
|
||||||
flags.AddFlagSet(&imageFlags)
|
|
||||||
flags.AddFlagSet(&retryFlags)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ func loginCmd(global *globalOptions) *cobra.Command {
|
|||||||
}
|
}
|
||||||
adjustUsage(cmd)
|
adjustUsage(cmd)
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
commonFlag.OptionalBoolFlag(flags, &opts.tlsVerify, "tls-verify", "require HTTPS and verify certificates when accessing the registry")
|
|
||||||
flags.AddFlagSet(auth.GetLoginFlags(&opts.loginOpts))
|
flags.AddFlagSet(auth.GetLoginFlags(&opts.loginOpts))
|
||||||
|
commonFlag.OptionalBoolFlag(flags, &opts.tlsVerify, "tls-verify", "require HTTPS and verify certificates when accessing the registry")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ func logoutCmd(global *globalOptions) *cobra.Command {
|
|||||||
}
|
}
|
||||||
adjustUsage(cmd)
|
adjustUsage(cmd)
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
commonFlag.OptionalBoolFlag(flags, &opts.tlsVerify, "tls-verify", "require HTTPS and verify certificates when accessing the registry")
|
|
||||||
flags.AddFlagSet(auth.GetLogoutFlags(&opts.logoutOpts))
|
flags.AddFlagSet(auth.GetLogoutFlags(&opts.logoutOpts))
|
||||||
|
commonFlag.OptionalBoolFlag(flags, &opts.tlsVerify, "tls-verify", "require HTTPS and verify certificates when accessing the registry")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,16 +14,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
commonFlag "github.com/containers/common/pkg/flag"
|
|
||||||
"github.com/containers/common/pkg/retry"
|
"github.com/containers/common/pkg/retry"
|
||||||
"github.com/containers/image/v5/copy"
|
"github.com/containers/image/v5/copy"
|
||||||
"github.com/containers/image/v5/directory"
|
"github.com/containers/image/v5/directory"
|
||||||
"github.com/containers/image/v5/docker"
|
"github.com/containers/image/v5/docker"
|
||||||
"github.com/containers/image/v5/docker/reference"
|
"github.com/containers/image/v5/docker/reference"
|
||||||
"github.com/containers/image/v5/manifest"
|
"github.com/containers/image/v5/manifest"
|
||||||
"github.com/containers/image/v5/pkg/cli"
|
|
||||||
"github.com/containers/image/v5/pkg/cli/sigstore"
|
|
||||||
"github.com/containers/image/v5/signature/signer"
|
|
||||||
"github.com/containers/image/v5/transports"
|
"github.com/containers/image/v5/transports"
|
||||||
"github.com/containers/image/v5/types"
|
"github.com/containers/image/v5/types"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
@ -34,26 +30,20 @@ import (
|
|||||||
|
|
||||||
// syncOptions contains information retrieved from the skopeo sync command line.
|
// syncOptions contains information retrieved from the skopeo sync command line.
|
||||||
type syncOptions struct {
|
type syncOptions struct {
|
||||||
global *globalOptions // Global (not command dependent) skopeo options
|
global *globalOptions // Global (not command dependent) skopeo options
|
||||||
deprecatedTLSVerify *deprecatedTLSVerifyOption
|
deprecatedTLSVerify *deprecatedTLSVerifyOption
|
||||||
srcImage *imageOptions // Source image options
|
srcImage *imageOptions // Source image options
|
||||||
destImage *imageDestOptions // Destination image options
|
destImage *imageDestOptions // Destination image options
|
||||||
retryOpts *retry.Options
|
retryOpts *retry.Options
|
||||||
removeSignatures bool // Do not copy signatures from the source image
|
copy *sharedCopyOptions
|
||||||
signByFingerprint string // Sign the image using a GPG key with the specified fingerprint
|
source string // Source repository name
|
||||||
signBySigstoreParamFile string // Sign the image using a sigstore signature per configuration in a param file
|
destination string // Destination registry name
|
||||||
signBySigstorePrivateKey string // Sign the image using a sigstore private key
|
digestFile string // Write digest to this file
|
||||||
signPassphraseFile string // Path pointing to a passphrase file when signing
|
scoped bool // When true, namespace copied images at destination using the source repository name
|
||||||
format commonFlag.OptionalString // Force conversion of the image to a specified format
|
all bool // Copy all of the images if an image in the source is a list
|
||||||
source string // Source repository name
|
dryRun bool // Don't actually copy anything, just output what it would have done
|
||||||
destination string // Destination registry name
|
keepGoing bool // Whether or not to abort the sync if there are any errors during syncing the images
|
||||||
digestFile string // Write digest to this file
|
appendSuffix string // Suffix to append to destination image tag
|
||||||
scoped bool // When true, namespace copied images at destination using the source repository name
|
|
||||||
all bool // Copy all of the images if an image in the source is a list
|
|
||||||
dryRun bool // Don't actually copy anything, just output what it would have done
|
|
||||||
preserveDigests bool // Preserve digests during sync
|
|
||||||
keepGoing bool // Whether or not to abort the sync if there are any errors during syncing the images
|
|
||||||
appendSuffix string // Suffix to append to destination image tag
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// repoDescriptor contains information of a single repository used as a sync source.
|
// repoDescriptor contains information of a single repository used as a sync source.
|
||||||
@ -89,6 +79,7 @@ func syncCmd(global *globalOptions) *cobra.Command {
|
|||||||
srcFlags, srcOpts := dockerImageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "src-", "screds")
|
srcFlags, srcOpts := dockerImageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "src-", "screds")
|
||||||
destFlags, destOpts := dockerImageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "dest-", "dcreds")
|
destFlags, destOpts := dockerImageFlags(global, sharedOpts, deprecatedTLSVerifyOpt, "dest-", "dcreds")
|
||||||
retryFlags, retryOpts := retryFlags()
|
retryFlags, retryOpts := retryFlags()
|
||||||
|
copyFlags, copyOpts := sharedCopyFlags()
|
||||||
|
|
||||||
opts := syncOptions{
|
opts := syncOptions{
|
||||||
global: global,
|
global: global,
|
||||||
@ -96,6 +87,7 @@ func syncCmd(global *globalOptions) *cobra.Command {
|
|||||||
srcImage: srcOpts,
|
srcImage: srcOpts,
|
||||||
destImage: &imageDestOptions{imageOptions: destOpts},
|
destImage: &imageDestOptions{imageOptions: destOpts},
|
||||||
retryOpts: retryOpts,
|
retryOpts: retryOpts,
|
||||||
|
copy: copyOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@ -113,12 +105,12 @@ See skopeo-sync(1) for details.
|
|||||||
}
|
}
|
||||||
adjustUsage(cmd)
|
adjustUsage(cmd)
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from SOURCE images")
|
flags.AddFlagSet(&sharedFlags)
|
||||||
flags.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`")
|
flags.AddFlagSet(&deprecatedTLSVerifyFlags)
|
||||||
flags.StringVar(&opts.signBySigstoreParamFile, "sign-by-sigstore", "", "Sign the image using a sigstore parameter file at `PATH`")
|
flags.AddFlagSet(&srcFlags)
|
||||||
flags.StringVar(&opts.signBySigstorePrivateKey, "sign-by-sigstore-private-key", "", "Sign the image using a sigstore private key at `PATH`")
|
flags.AddFlagSet(&destFlags)
|
||||||
flags.StringVar(&opts.signPassphraseFile, "sign-passphrase-file", "", "File that contains a passphrase for the --sign-by key")
|
flags.AddFlagSet(&retryFlags)
|
||||||
flags.VarP(commonFlag.NewOptionalStringValue(&opts.format), "format", "f", `MANIFEST TYPE (oci, v2s1, or v2s2) to use when syncing image(s) to a destination (default is manifest type of source, with fallbacks)`)
|
flags.AddFlagSet(©Flags)
|
||||||
flags.StringVarP(&opts.source, "src", "s", "", "SOURCE transport type")
|
flags.StringVarP(&opts.source, "src", "s", "", "SOURCE transport type")
|
||||||
flags.StringVarP(&opts.destination, "dest", "d", "", "DESTINATION transport type")
|
flags.StringVarP(&opts.destination, "dest", "d", "", "DESTINATION transport type")
|
||||||
flags.BoolVar(&opts.scoped, "scoped", false, "Images at DESTINATION are prefix using the full source image path as scope")
|
flags.BoolVar(&opts.scoped, "scoped", false, "Images at DESTINATION are prefix using the full source image path as scope")
|
||||||
@ -126,13 +118,7 @@ See skopeo-sync(1) for details.
|
|||||||
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digests and Image References of the resulting images to the specified file, separated by newlines")
|
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digests and Image References of the resulting images to the specified file, separated by newlines")
|
||||||
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
|
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
|
||||||
flags.BoolVar(&opts.dryRun, "dry-run", false, "Run without actually copying data")
|
flags.BoolVar(&opts.dryRun, "dry-run", false, "Run without actually copying data")
|
||||||
flags.BoolVar(&opts.preserveDigests, "preserve-digests", false, "Preserve digests of images and lists")
|
|
||||||
flags.BoolVarP(&opts.keepGoing, "keep-going", "", false, "Do not abort the sync if any image copy fails")
|
flags.BoolVarP(&opts.keepGoing, "keep-going", "", false, "Do not abort the sync if any image copy fails")
|
||||||
flags.AddFlagSet(&sharedFlags)
|
|
||||||
flags.AddFlagSet(&deprecatedTLSVerifyFlags)
|
|
||||||
flags.AddFlagSet(&srcFlags)
|
|
||||||
flags.AddFlagSet(&destFlags)
|
|
||||||
flags.AddFlagSet(&retryFlags)
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,14 +629,6 @@ func (opts *syncOptions) run(args []string, stdout io.Writer) (retErr error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestType string
|
|
||||||
if opts.format.Present() {
|
|
||||||
manifestType, err = parseManifestFormat(opts.format.Value())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := opts.global.commandTimeoutContext()
|
ctx, cancel := opts.global.commandTimeoutContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -669,57 +647,15 @@ func (opts *syncOptions) run(args []string, stdout io.Writer) (retErr error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// c/image/copy.Image does allow creating both simple signing and sigstore signatures simultaneously,
|
options, cleanupOptions, err := opts.copy.copyOptions(stdout)
|
||||||
// with independent passphrases, but that would make the CLI probably too confusing.
|
if err != nil {
|
||||||
// For now, use the passphrase with either, but only one of them.
|
return err
|
||||||
if opts.signPassphraseFile != "" && opts.signByFingerprint != "" && opts.signBySigstorePrivateKey != "" {
|
|
||||||
return fmt.Errorf("Only one of --sign-by and sign-by-sigstore-private-key can be used with sign-passphrase-file")
|
|
||||||
}
|
|
||||||
var passphrase string
|
|
||||||
if opts.signPassphraseFile != "" {
|
|
||||||
p, err := cli.ReadPassphraseFile(opts.signPassphraseFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
passphrase = p
|
|
||||||
} else if opts.signBySigstorePrivateKey != "" {
|
|
||||||
p, err := promptForPassphrase(opts.signBySigstorePrivateKey, os.Stdin, os.Stdout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
passphrase = p
|
|
||||||
}
|
}
|
||||||
|
defer cleanupOptions()
|
||||||
|
options.DestinationCtx = destinationCtx
|
||||||
|
options.ImageListSelection = imageListSelection
|
||||||
|
options.OptimizeDestinationImageAlreadyExists = true
|
||||||
|
|
||||||
var signers []*signer.Signer
|
|
||||||
if opts.signBySigstoreParamFile != "" {
|
|
||||||
signer, err := sigstore.NewSignerFromParameterFile(opts.signBySigstoreParamFile, &sigstore.Options{
|
|
||||||
PrivateKeyPassphrasePrompt: func(keyFile string) (string, error) {
|
|
||||||
return promptForPassphrase(keyFile, os.Stdin, os.Stdout)
|
|
||||||
},
|
|
||||||
Stdin: os.Stdin,
|
|
||||||
Stdout: stdout,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error using --sign-by-sigstore: %w", err)
|
|
||||||
}
|
|
||||||
defer signer.Close()
|
|
||||||
signers = append(signers, signer)
|
|
||||||
}
|
|
||||||
|
|
||||||
options := copy.Options{
|
|
||||||
RemoveSignatures: opts.removeSignatures,
|
|
||||||
Signers: signers,
|
|
||||||
SignBy: opts.signByFingerprint,
|
|
||||||
SignPassphrase: passphrase,
|
|
||||||
SignBySigstorePrivateKeyFile: opts.signBySigstorePrivateKey,
|
|
||||||
SignSigstorePrivateKeyPassphrase: []byte(passphrase),
|
|
||||||
ReportWriter: stdout,
|
|
||||||
DestinationCtx: destinationCtx,
|
|
||||||
ImageListSelection: imageListSelection,
|
|
||||||
PreserveDigests: opts.preserveDigests,
|
|
||||||
OptimizeDestinationImageAlreadyExists: true,
|
|
||||||
ForceManifestMIMEType: manifestType,
|
|
||||||
}
|
|
||||||
errorsPresent := false
|
errorsPresent := false
|
||||||
imagesNumber := 0
|
imagesNumber := 0
|
||||||
if opts.dryRun {
|
if opts.dryRun {
|
||||||
@ -775,7 +711,7 @@ func (opts *syncOptions) run(args []string, stdout io.Writer) (retErr error) {
|
|||||||
} else {
|
} else {
|
||||||
logrus.WithFields(fromToFields).Infof("Copying image ref %d/%d", counter+1, len(srcRepo.ImageRefs))
|
logrus.WithFields(fromToFields).Infof("Copying image ref %d/%d", counter+1, len(srcRepo.ImageRefs))
|
||||||
if err = retry.IfNecessary(ctx, func() error {
|
if err = retry.IfNecessary(ctx, func() error {
|
||||||
manifestBytes, err = copy.Image(ctx, policyContext, destRef, ref, &options)
|
manifestBytes, err = copy.Image(ctx, policyContext, destRef, ref, options)
|
||||||
return err
|
return err
|
||||||
}, opts.retryOpts); err != nil {
|
}, opts.retryOpts); err != nil {
|
||||||
if !opts.keepGoing {
|
if !opts.keepGoing {
|
||||||
|
@ -11,11 +11,15 @@ import (
|
|||||||
|
|
||||||
commonFlag "github.com/containers/common/pkg/flag"
|
commonFlag "github.com/containers/common/pkg/flag"
|
||||||
"github.com/containers/common/pkg/retry"
|
"github.com/containers/common/pkg/retry"
|
||||||
|
"github.com/containers/image/v5/copy"
|
||||||
"github.com/containers/image/v5/directory"
|
"github.com/containers/image/v5/directory"
|
||||||
"github.com/containers/image/v5/manifest"
|
"github.com/containers/image/v5/manifest"
|
||||||
ociarchive "github.com/containers/image/v5/oci/archive"
|
ociarchive "github.com/containers/image/v5/oci/archive"
|
||||||
ocilayout "github.com/containers/image/v5/oci/layout"
|
ocilayout "github.com/containers/image/v5/oci/layout"
|
||||||
|
"github.com/containers/image/v5/pkg/cli"
|
||||||
|
"github.com/containers/image/v5/pkg/cli/sigstore"
|
||||||
"github.com/containers/image/v5/pkg/compression"
|
"github.com/containers/image/v5/pkg/compression"
|
||||||
|
"github.com/containers/image/v5/signature/signer"
|
||||||
"github.com/containers/image/v5/storage"
|
"github.com/containers/image/v5/storage"
|
||||||
"github.com/containers/image/v5/transports/alltransports"
|
"github.com/containers/image/v5/transports/alltransports"
|
||||||
"github.com/containers/image/v5/types"
|
"github.com/containers/image/v5/types"
|
||||||
@ -176,9 +180,9 @@ func imageFlags(global *globalOptions, shared *sharedImageOptions, deprecatedTLS
|
|||||||
dockerFlags, opts := dockerImageFlags(global, shared, deprecatedTLSVerify, flagPrefix, credsOptionAlias)
|
dockerFlags, opts := dockerImageFlags(global, shared, deprecatedTLSVerify, flagPrefix, credsOptionAlias)
|
||||||
|
|
||||||
fs := pflag.FlagSet{}
|
fs := pflag.FlagSet{}
|
||||||
|
fs.AddFlagSet(&dockerFlags)
|
||||||
fs.StringVar(&opts.sharedBlobDir, flagPrefix+"shared-blob-dir", "", "`DIRECTORY` to use to share blobs across OCI repositories")
|
fs.StringVar(&opts.sharedBlobDir, flagPrefix+"shared-blob-dir", "", "`DIRECTORY` to use to share blobs across OCI repositories")
|
||||||
fs.StringVar(&opts.dockerDaemonHost, flagPrefix+"daemon-host", "", "use docker daemon host at `HOST` (docker-daemon: only)")
|
fs.StringVar(&opts.dockerDaemonHost, flagPrefix+"daemon-host", "", "use docker daemon host at `HOST` (docker-daemon: only)")
|
||||||
fs.AddFlagSet(&dockerFlags)
|
|
||||||
return fs, opts
|
return fs, opts
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +323,110 @@ func (opts *imageDestOptions) warnAboutIneffectiveOptions(destTransport types.Im
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sharedCopyOptions collects CLI flags that affect copying images, currently shared between the copy and sync commands.
|
||||||
|
type sharedCopyOptions struct {
|
||||||
|
removeSignatures bool // Do not copy signatures from the source image
|
||||||
|
signByFingerprint string // Sign the image using a GPG key with the specified fingerprint
|
||||||
|
signBySigstoreParamFile string // Sign the image using a sigstore signature per configuration in a param file
|
||||||
|
signBySigstorePrivateKey string // Sign the image using a sigstore private key
|
||||||
|
signPassphraseFile string // Path pointing to a passphrase file when signing
|
||||||
|
preserveDigests bool // Preserve digests during copy
|
||||||
|
format commonFlag.OptionalString // Force conversion of the image to a specified format
|
||||||
|
}
|
||||||
|
|
||||||
|
// sharedCopyFlags prepares a collection of CLI flags writing into sharedCopyoptions.
|
||||||
|
func sharedCopyFlags() (pflag.FlagSet, *sharedCopyOptions) {
|
||||||
|
opts := sharedCopyOptions{}
|
||||||
|
fs := pflag.FlagSet{}
|
||||||
|
fs.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from source")
|
||||||
|
fs.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`")
|
||||||
|
fs.StringVar(&opts.signBySigstoreParamFile, "sign-by-sigstore", "", "Sign the image using a sigstore parameter file at `PATH`")
|
||||||
|
fs.StringVar(&opts.signBySigstorePrivateKey, "sign-by-sigstore-private-key", "", "Sign the image using a sigstore private key at `PATH`")
|
||||||
|
fs.StringVar(&opts.signPassphraseFile, "sign-passphrase-file", "", "Read a passphrase for signing an image from `PATH`")
|
||||||
|
fs.VarP(commonFlag.NewOptionalStringValue(&opts.format), "format", "f", `MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifest type of source, with fallbacks)`)
|
||||||
|
fs.BoolVar(&opts.preserveDigests, "preserve-digests", false, "Preserve digests of images and lists")
|
||||||
|
return fs, &opts
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyOptions interprets opts, returns a partially-filled *copy.Options,
|
||||||
|
// and a function that should be called to clean up.
|
||||||
|
func (opts *sharedCopyOptions) copyOptions(stdout io.Writer) (*copy.Options, func(), error) {
|
||||||
|
var manifestType string
|
||||||
|
if opts.format.Present() {
|
||||||
|
mt, err := parseManifestFormat(opts.format.Value())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
manifestType = mt
|
||||||
|
}
|
||||||
|
|
||||||
|
// c/image/copy.Image does allow creating both simple signing and sigstore signatures simultaneously,
|
||||||
|
// with independent passphrases, but that would make the CLI probably too confusing.
|
||||||
|
// For now, use the passphrase with either, but only one of them.
|
||||||
|
if opts.signPassphraseFile != "" && opts.signByFingerprint != "" && opts.signBySigstorePrivateKey != "" {
|
||||||
|
return nil, nil, fmt.Errorf("Only one of --sign-by and sign-by-sigstore-private-key can be used with sign-passphrase-file")
|
||||||
|
}
|
||||||
|
var passphrase string
|
||||||
|
if opts.signPassphraseFile != "" {
|
||||||
|
p, err := cli.ReadPassphraseFile(opts.signPassphraseFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
passphrase = p
|
||||||
|
} else if opts.signBySigstorePrivateKey != "" {
|
||||||
|
p, err := promptForPassphrase(opts.signBySigstorePrivateKey, os.Stdin, os.Stdout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
passphrase = p
|
||||||
|
} // opts.signByFingerprint triggers a GPG-agent passphrase prompt, possibly using a more secure channel, so we usually shouldn’t prompt ourselves if no passphrase was explicitly provided.
|
||||||
|
var passphraseBytes []byte
|
||||||
|
if passphrase != "" {
|
||||||
|
passphraseBytes = []byte(passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signers []*signer.Signer
|
||||||
|
closeSigners := func() {
|
||||||
|
for _, signer := range signers {
|
||||||
|
signer.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
succeeded := false
|
||||||
|
defer func() {
|
||||||
|
if !succeeded {
|
||||||
|
closeSigners()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if opts.signBySigstoreParamFile != "" {
|
||||||
|
signer, err := sigstore.NewSignerFromParameterFile(opts.signBySigstoreParamFile, &sigstore.Options{
|
||||||
|
PrivateKeyPassphrasePrompt: func(keyFile string) (string, error) {
|
||||||
|
return promptForPassphrase(keyFile, os.Stdin, os.Stdout)
|
||||||
|
},
|
||||||
|
Stdin: os.Stdin,
|
||||||
|
Stdout: stdout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Error using --sign-by-sigstore: %w", err)
|
||||||
|
}
|
||||||
|
signers = append(signers, signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
succeeded = true
|
||||||
|
return ©.Options{
|
||||||
|
RemoveSignatures: opts.removeSignatures,
|
||||||
|
Signers: signers,
|
||||||
|
SignBy: opts.signByFingerprint,
|
||||||
|
SignPassphrase: passphrase,
|
||||||
|
SignBySigstorePrivateKeyFile: opts.signBySigstorePrivateKey,
|
||||||
|
SignSigstorePrivateKeyPassphrase: passphraseBytes,
|
||||||
|
|
||||||
|
ReportWriter: stdout,
|
||||||
|
|
||||||
|
PreserveDigests: opts.preserveDigests,
|
||||||
|
ForceManifestMIMEType: manifestType,
|
||||||
|
}, closeSigners, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseCreds(creds string) (string, string, error) {
|
func parseCreds(creds string) (string, string, error) {
|
||||||
if creds == "" {
|
if creds == "" {
|
||||||
return "", "", errors.New("credentials can't be empty")
|
return "", "", errors.New("credentials can't be empty")
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/copy"
|
||||||
"github.com/containers/image/v5/manifest"
|
"github.com/containers/image/v5/manifest"
|
||||||
"github.com/containers/image/v5/types"
|
"github.com/containers/image/v5/types"
|
||||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
@ -350,6 +353,92 @@ func TestTLSVerifyFlags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fakeSharedCopyOptions creates sharedCopyOptions and sets it according to cmdFlags.
|
||||||
|
func fakeSharedCopyOptions(t *testing.T, cmdFlags []string) *sharedCopyOptions {
|
||||||
|
_, cmd := fakeGlobalOptions(t, []string{})
|
||||||
|
sharedCopyFlags, sharedCopyOpts := sharedCopyFlags()
|
||||||
|
cmd.Flags().AddFlagSet(&sharedCopyFlags)
|
||||||
|
err := cmd.ParseFlags(cmdFlags)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return sharedCopyOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSharedCopyOptionsCopyOptions(t *testing.T) {
|
||||||
|
someStdout := bytes.Buffer{}
|
||||||
|
|
||||||
|
// Default state
|
||||||
|
opts := fakeSharedCopyOptions(t, []string{})
|
||||||
|
res, cleanup, err := opts.copyOptions(&someStdout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cleanup()
|
||||||
|
assert.Equal(t, ©.Options{
|
||||||
|
ReportWriter: &someStdout,
|
||||||
|
}, res)
|
||||||
|
|
||||||
|
// Set most flags to non-default values
|
||||||
|
// This should also test --sign-by-sigstore and --sign-by-sigstore-private-key; we would have
|
||||||
|
// to create test keys for that.
|
||||||
|
opts = fakeSharedCopyOptions(t, []string{
|
||||||
|
"--remove-signatures",
|
||||||
|
"--sign-by", "gpgFingerprint",
|
||||||
|
"--format", "oci",
|
||||||
|
"--preserve-digests",
|
||||||
|
})
|
||||||
|
res, cleanup, err = opts.copyOptions(&someStdout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cleanup()
|
||||||
|
assert.Equal(t, ©.Options{
|
||||||
|
RemoveSignatures: true,
|
||||||
|
SignBy: "gpgFingerprint",
|
||||||
|
ReportWriter: &someStdout,
|
||||||
|
PreserveDigests: true,
|
||||||
|
ForceManifestMIMEType: imgspecv1.MediaTypeImageManifest,
|
||||||
|
}, res)
|
||||||
|
|
||||||
|
// --sign-passphrase-file + --sign-by work
|
||||||
|
passphraseFile, err := os.CreateTemp("", "passphrase") // Eventually we could refer to a passphrase fixture instead
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Remove(passphraseFile.Name())
|
||||||
|
_, err = passphraseFile.WriteString("test-passphrase")
|
||||||
|
require.NoError(t, err)
|
||||||
|
opts = fakeSharedCopyOptions(t, []string{
|
||||||
|
"--sign-by", "gpgFingerprint",
|
||||||
|
"--sign-passphrase-file", passphraseFile.Name(),
|
||||||
|
})
|
||||||
|
res, cleanup, err = opts.copyOptions(&someStdout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cleanup()
|
||||||
|
assert.Equal(t, ©.Options{
|
||||||
|
SignBy: "gpgFingerprint",
|
||||||
|
SignPassphrase: "test-passphrase",
|
||||||
|
SignSigstorePrivateKeyPassphrase: []byte("test-passphrase"),
|
||||||
|
ReportWriter: &someStdout,
|
||||||
|
}, res)
|
||||||
|
// --sign-passphrase-file + --sign-by-sigstore-private-key should be tested here.
|
||||||
|
|
||||||
|
// Invalid --format
|
||||||
|
opts = fakeSharedCopyOptions(t, []string{"--format", "invalid"})
|
||||||
|
_, _, err = opts.copyOptions(&someStdout)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// More --sign-passphrase-file, --sign-by-sigstore-private-key, --sign-by-sigstore failure cases should be tested here.
|
||||||
|
|
||||||
|
// --sign-passphrase-file not found
|
||||||
|
opts = fakeSharedCopyOptions(t, []string{
|
||||||
|
"--sign-by", "gpgFingerprint",
|
||||||
|
"--sign-passphrase-file", "/dev/null/this/does/not/exist",
|
||||||
|
})
|
||||||
|
_, _, err = opts.copyOptions(&someStdout)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// --sign-by-sigstore file not found
|
||||||
|
opts = fakeSharedCopyOptions(t, []string{
|
||||||
|
"--sign-by-sigstore", "/dev/null/this/does/not/exist",
|
||||||
|
})
|
||||||
|
_, _, err = opts.copyOptions(&someStdout)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseManifestFormat(t *testing.T) {
|
func TestParseManifestFormat(t *testing.T) {
|
||||||
for _, testCase := range []struct {
|
for _, testCase := range []struct {
|
||||||
formatParam string
|
formatParam string
|
||||||
|
Loading…
Reference in New Issue
Block a user