diff --git a/cmd/docker-notation/sign.go b/cmd/docker-notation/sign.go index fa135a368..54e9a04d4 100644 --- a/cmd/docker-notation/sign.go +++ b/cmd/docker-notation/sign.go @@ -32,7 +32,15 @@ var signCommand = &cli.Command{ } func signImage(ctx *cli.Context) error { - signer, err := cmd.GetSigner(ctx) + // TODO: make this change only to make sure the code can be compiled + // According to the https://github.com/notaryproject/notation/discussions/251, + // we can update/deprecate it later + signerOpts := &cmd.SignerFlagOpts{ + Key: ctx.String(cmd.FlagKey.Name), + KeyFile: ctx.String(cmd.FlagKeyFile.Name), + CertFile: ctx.String(cmd.FlagCertFile.Name), + } + signer, err := cmd.GetSigner(signerOpts) if err != nil { return err } @@ -55,7 +63,7 @@ func signImage(ctx *cli.Context) error { } } sig, err := signer.Sign(ctx.Context, desc, notation.SignOptions{ - Expiry: cmd.GetExpiry(ctx), + Expiry: cmd.GetExpiry(ctx.Duration(cmd.FlagExpiry.Name)), }) if err != nil { return err diff --git a/cmd/notation/cache.go b/cmd/notation/cache.go index f42fd612c..def604d22 100644 --- a/cmd/notation/cache.go +++ b/cmd/notation/cache.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "context" "errors" "fmt" "io/fs" @@ -11,80 +12,118 @@ import ( "github.com/notaryproject/notation/pkg/config" "github.com/opencontainers/go-digest" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" "oras.land/oras-go/v2/registry" ) -var ( - cacheCommand = &cli.Command{ - Name: "cache", - Usage: "Manage signature cache", - Subcommands: []*cli.Command{ - cacheListCommand, - cachePruneCommand, - cacheRemoveCommand, - }, +type cacheListOpts struct { + RemoteFlagOpts + reference string +} + +type cachePruneOpts struct { + RemoteFlagOpts + references []string + all bool + purge bool + force bool +} + +type cacheRemoveOpts struct { + RemoteFlagOpts + reference string + sigDigests []string +} + +func cacheCommand() *cobra.Command { + command := &cobra.Command{ + Use: "cache", + Short: "Manage signature cache", } + command.AddCommand(cacheListCommand(nil), cachePruneCommand(nil), cacheRemoveCommand(nil)) + return command +} - cacheListCommand = &cli.Command{ - Name: "list", - Usage: "List signatures in cache", +func cacheListCommand(opts *cacheListOpts) *cobra.Command { + if opts == nil { + opts = &cacheListOpts{} + } + command := &cobra.Command{ + Use: "list [reference|manifest_digest]", Aliases: []string{"ls"}, - Flags: []cli.Flag{ - flagLocal, - flagUsername, - flagPassword, + Short: "List signatures in cache", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + opts.reference = args[0] + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return listCachedSignatures(cmd, opts) }, - ArgsUsage: "[reference|manifest_digest]", - Action: listCachedSignatures, } + opts.ApplyFlags(command.Flags()) + return command +} - cachePruneCommand = &cli.Command{ - Name: "prune", - Usage: "Prune signature from cache", - ArgsUsage: "[reference|manifest_digest] ...", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "all", - Aliases: []string{"a"}, - Usage: "prune all cached signatures", - }, - &cli.BoolFlag{ - Name: "purge", - Usage: "remove the signature directory, combined with --all", - }, - &cli.BoolFlag{ - Name: "force", - Aliases: []string{"f"}, - Usage: "do not prompt for confirmation", - }, - flagLocal, - flagUsername, - flagPassword, +func cachePruneCommand(opts *cachePruneOpts) *cobra.Command { + if opts == nil { + opts = &cachePruneOpts{} + } + command := &cobra.Command{ + Use: "prune [reference|manifest_digest]...", + Short: "Prune signature from cache", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("nothing to prune") + } + opts.references = args + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return pruneCachedSignatures(cmd, opts) }, - Action: pruneCachedSignatures, } + command.Flags().BoolVarP(&opts.all, "all", "a", false, "prune all cached signatures") + command.Flags().BoolVar(&opts.purge, "purge", false, "remove the signature directory, combined with --all") + command.Flags().BoolVarP(&opts.force, "force", "f", false, "do not prompt for confirmation") + opts.ApplyFlags(command.Flags()) + return command +} - cacheRemoveCommand = &cli.Command{ - Name: "remove", - Usage: "Remove signature from cache", - Aliases: []string{"rm"}, - ArgsUsage: " ...", - Flags: []cli.Flag{ - flagLocal, - flagUsername, - flagPassword, +func cacheRemoveCommand(opts *cacheRemoveOpts) *cobra.Command { + if opts == nil { + opts = &cacheRemoveOpts{} + } + command := &cobra.Command{ + Use: "remove [reference|manifest_digest] [signature_digest]...", + Aliases: []string{"rm"}, + Short: "Remove signature from cache", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing target manifest") + } + opts.reference = args[0] + if len(args) == 1 { + return errors.New("no signature specified") + } + opts.sigDigests = args[1:] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return removeCachedSignatures(cmd, opts) }, - Action: removeCachedSignatures, } -) + opts.ApplyFlags(command.Flags()) + return command +} -func listCachedSignatures(ctx *cli.Context) error { - if !ctx.Args().Present() { +func listCachedSignatures(command *cobra.Command, opts *cacheListOpts) error { + if command.Flags().NArg() == 0 { return listManifestsWithCachedSignature() } - manifestDigest, err := getManifestDigestFromContext(ctx, ctx.Args().First()) + manifestDigest, err := getManifestDigestFromContext(command.Context(), &opts.RemoteFlagOpts, opts.reference) if err != nil { return err } @@ -113,12 +152,12 @@ func listManifestsWithCachedSignature() error { }) } -func pruneCachedSignatures(ctx *cli.Context) error { - if ctx.Bool("all") { - if !ctx.Bool("force") { +func pruneCachedSignatures(command *cobra.Command, opts *cachePruneOpts) error { + if opts.all { + if !opts.force { fmt.Println("WARNING! This will remove:") fmt.Println("- all cached signatures") - if ctx.Bool("purge") { + if opts.purge { fmt.Println("- all files in the cache signature directory") } fmt.Println() @@ -144,17 +183,13 @@ func pruneCachedSignatures(ctx *cli.Context) error { ); err != nil { return err } - if ctx.Bool("purge") { + if opts.purge { return os.RemoveAll(config.SignatureStoreDirPath) } return nil } - - if !ctx.Args().Present() { - return errors.New("nothing to prune") - } - refs := ctx.Args().Slice() - if !ctx.Bool("force") { + refs := opts.references + if !opts.force { fmt.Println("WARNING! This will remove cached signatures for manifests below:") for _, ref := range refs { fmt.Println("-", ref) @@ -165,7 +200,7 @@ func pruneCachedSignatures(ctx *cli.Context) error { } } for _, ref := range refs { - manifestDigest, err := getManifestDigestFromContext(ctx, ref) + manifestDigest, err := getManifestDigestFromContext(command.Context(), &opts.RemoteFlagOpts, ref) if err != nil { return err } @@ -180,24 +215,15 @@ func pruneCachedSignatures(ctx *cli.Context) error { return nil } -func removeCachedSignatures(ctx *cli.Context) error { +func removeCachedSignatures(command *cobra.Command, opts *cacheRemoveOpts) error { // initialize - sigDigests := ctx.Args().Slice() - if len(sigDigests) == 0 { - return errors.New("missing target manifest") - } - sigDigests = sigDigests[1:] - if len(sigDigests) == 0 { - return errors.New("no signature specified") - } - - manifestDigest, err := getManifestDigestFromContext(ctx, ctx.Args().First()) + manifestDigest, err := getManifestDigestFromContext(command.Context(), &opts.RemoteFlagOpts, opts.reference) if err != nil { return err } // core process - for _, sigDigest := range sigDigests { + for _, sigDigest := range opts.sigDigests { path := config.SignaturePath(manifestDigest, digest.Digest(sigDigest)) if err := os.Remove(path); err != nil { return err @@ -235,7 +261,7 @@ func walkCachedSignatureTree(root string, fn func(algorithm string, encodedEntry return nil } -func getManifestDigestFromContext(ctx *cli.Context, ref string) (manifestDigest digest.Digest, err error) { +func getManifestDigestFromContext(ctx context.Context, opts *RemoteFlagOpts, ref string) (manifestDigest digest.Digest, err error) { manifestDigest, err = digest.Parse(ref) if err == nil { return @@ -250,7 +276,7 @@ func getManifestDigestFromContext(ctx *cli.Context, ref string) (manifestDigest return } - manifest, err := getManifestDescriptorFromContextWithReference(ctx, ref) + manifest, err := getManifestDescriptorFromContextWithReference(ctx, opts, ref) if err != nil { return } diff --git a/cmd/notation/cert.go b/cmd/notation/cert.go index fe92eaa1c..b476adf66 100644 --- a/cmd/notation/cert.go +++ b/cmd/notation/cert.go @@ -11,94 +11,126 @@ import ( "github.com/notaryproject/notation/internal/ioutil" "github.com/notaryproject/notation/internal/slices" "github.com/notaryproject/notation/pkg/config" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) -var ( - certCommand = &cli.Command{ - Name: "certificate", +type certAddOpts struct { + path string + name string +} + +type certRemoveOpts struct { + names []string +} + +type certGenerateTestOpts struct { + name string + bits int + expiry time.Duration + trust bool + hosts []string + isDefault bool +} + +func certCommand() *cobra.Command { + command := &cobra.Command{ + Use: "certificate", Aliases: []string{"cert"}, - Usage: "Manage certificates used for verification", - Subcommands: []*cli.Command{ - certAddCommand, - certListCommand, - certRemoveCommand, - certGenerateTestCommand, - }, + Short: "Manage certificates used for verification", } - certAddCommand = &cli.Command{ - Name: "add", - Usage: "Add certificate to verification list", - ArgsUsage: "", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "certificate name", - }, + command.AddCommand(certAddCommand(nil), certListCommand(), certRemoveCommand(nil), certGenerateTestCommand(nil)) + return command +} + +func certAddCommand(opts *certAddOpts) *cobra.Command { + if opts == nil { + opts = &certAddOpts{} + } + command := &cobra.Command{ + Use: "add [path]", + Short: "Add certificate to verification list", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing certificate path") + } + opts.path = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return addCert(opts) }, - Action: addCert, } + command.Flags().StringVarP(&opts.name, "name", "n", "", "certificate name") + return command +} - certListCommand = &cli.Command{ - Name: "list", - Usage: "List certificates used for verification", +func certListCommand() *cobra.Command { + command := &cobra.Command{ + Use: "list", Aliases: []string{"ls"}, - Action: listCerts, - } - - certRemoveCommand = &cli.Command{ - Name: "remove", - Usage: "Remove certificate from the verification list", - Aliases: []string{"rm"}, - ArgsUsage: " ...", - Action: removeCerts, - } - - certGenerateTestCommand = &cli.Command{ - Name: "generate-test", - Usage: "Generates a test RSA key and a corresponding self-signed certificate", - ArgsUsage: " ...", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "key and certificate name", - }, - &cli.IntFlag{ - Name: "bits", - Usage: "RSA key bits", - Aliases: []string{"b"}, - Value: 2048, - }, - &cli.DurationFlag{ - Name: "expiry", - Aliases: []string{"e"}, - Usage: "certificate expiry", - Value: 365 * 24 * time.Hour, - }, - &cli.BoolFlag{ - Name: "trust", - Usage: "add the generated certificate to the verification list", - }, - keyDefaultFlag, + Short: "List certificates used for verification", + RunE: func(cmd *cobra.Command, args []string) error { + return listCerts() + }, + } + return command +} +func certRemoveCommand(opts *certRemoveOpts) *cobra.Command { + if opts == nil { + opts = &certRemoveOpts{} + } + command := &cobra.Command{ + Use: "remove [name]...", + Aliases: []string{"rm"}, + Short: "Remove certificate from the verification list", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing certificate names") + } + opts.names = args + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return removeCerts(opts) + }, + } + return command +} +func certGenerateTestCommand(opts *certGenerateTestOpts) *cobra.Command { + if opts == nil { + opts = &certGenerateTestOpts{} + } + command := &cobra.Command{ + Use: "generate-test [host]...", + Short: "Generates a test RSA key and a corresponding self-signed certificate", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing certificate hosts") + } + opts.hosts = args + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return generateTestCert(opts) }, - Action: generateTestCert, } -) -func addCert(ctx *cli.Context) error { + command.Flags().StringVarP(&opts.name, "name", "n", "", "key and certificate name") + command.Flags().IntVarP(&opts.bits, "bits", "b", 2048, "RSA key bits") + command.Flags().DurationVarP(&opts.expiry, "expiry", "e", 365*24*time.Hour, "certificate expiry") + command.Flags().BoolVar(&opts.trust, "trust", false, "add the generated certificate to the verification list") + setKeyDefaultFlag(command.Flags(), &opts.isDefault) + return command +} + +func addCert(opts *certAddOpts) error { // initialize - path := ctx.Args().First() - if path == "" { - return errors.New("missing certificate path") - } - path, err := filepath.Abs(path) + path, err := filepath.Abs(opts.path) if err != nil { return err } - name := ctx.String("name") + name := opts.name // check if the target path is a cert if _, err := x509.ReadCertificateFile(path); err != nil { @@ -133,7 +165,7 @@ func addCertCore(cfg *config.File, name, path string) error { return nil } -func listCerts(ctx *cli.Context) error { +func listCerts() error { // core process cfg, err := config.LoadOrDefault() if err != nil { @@ -144,13 +176,7 @@ func listCerts(ctx *cli.Context) error { return ioutil.PrintCertificateMap(os.Stdout, cfg.VerificationCertificates.Certificates) } -func removeCerts(ctx *cli.Context) error { - // initialize - names := ctx.Args().Slice() - if len(names) == 0 { - return errors.New("missing certificate names") - } - +func removeCerts(opts *certRemoveOpts) error { // core process cfg, err := config.LoadOrDefault() if err != nil { @@ -158,7 +184,7 @@ func removeCerts(ctx *cli.Context) error { } var removedNames []string - for _, name := range names { + for _, name := range opts.names { idx := slices.Index(cfg.VerificationCertificates.Certificates, name) if idx < 0 { return errors.New(name + ": not found") diff --git a/cmd/notation/cert_gen.go b/cmd/notation/cert_gen.go index d27c434cc..00479f9bc 100644 --- a/cmd/notation/cert_gen.go +++ b/cmd/notation/cert_gen.go @@ -7,7 +7,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" - "errors" "fmt" "math/big" "net" @@ -15,22 +14,18 @@ import ( "github.com/notaryproject/notation/internal/osutil" "github.com/notaryproject/notation/pkg/config" - "github.com/urfave/cli/v2" ) -func generateTestCert(ctx *cli.Context) error { +func generateTestCert(opts *certGenerateTestOpts) error { // initialize - hosts := ctx.Args().Slice() - if len(hosts) == 0 { - return errors.New("missing certificate hosts") - } - name := ctx.String("name") + hosts := opts.hosts + name := opts.name if name == "" { name = hosts[0] } // generate RSA private key - bits := ctx.Int("bits") + bits := opts.bits fmt.Println("generating RSA Key with", bits, "bits") key, keyBytes, err := generateTestKey(bits) if err != nil { @@ -38,7 +33,7 @@ func generateTestCert(ctx *cli.Context) error { } // generate self-signed certificate - cert, certBytes, err := generateTestSelfSignedCert(key, hosts, ctx.Duration("expiry")) + cert, certBytes, err := generateTestSelfSignedCert(key, hosts, opts.expiry) if err != nil { return err } @@ -63,7 +58,7 @@ func generateTestCert(ctx *cli.Context) error { if err != nil { return err } - isDefault := ctx.Bool(keyDefaultFlag.Name) + isDefault := opts.isDefault keySuite := config.KeySuite{ Name: name, X509KeyPair: &config.X509KeyPair{ @@ -71,11 +66,11 @@ func generateTestCert(ctx *cli.Context) error { CertificatePath: certPath, }, } - err = addKeyCore(cfg, keySuite, ctx.Bool(keyDefaultFlag.Name)) + err = addKeyCore(cfg, keySuite, isDefault) if err != nil { return err } - trust := ctx.Bool("trust") + trust := opts.trust if trust { if err := addCertCore(cfg, name, certPath); err != nil { return err diff --git a/cmd/notation/common.go b/cmd/notation/common.go index 473295dc6..9d19890fb 100644 --- a/cmd/notation/common.go +++ b/cmd/notation/common.go @@ -1,43 +1,116 @@ package main -import "github.com/urfave/cli/v2" +import ( + "os" + + "github.com/spf13/pflag" +) + +const ( + defaultUsernameEnv = "NOTATION_USERNAME" + defaultPasswordEnv = "NOTATION_PASSWORD" + defaultMediaType = "application/vnd.docker.distribution.manifest.v2+json" +) var ( - flagUsername = &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username for registry operations", - EnvVars: []string{"NOTATION_USERNAME"}, - } - flagPassword = &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Usage: "Password for registry operations", - EnvVars: []string{"NOTATION_PASSWORD"}, - } - flagPlainHTTP = &cli.BoolFlag{ - Name: "plain-http", - Usage: "Registry access via plain HTTP", - } - flagMediaType = &cli.StringFlag{ - Name: "media-type", - Usage: "specify the media type of the manifest read from file or stdin", - Value: "application/vnd.docker.distribution.manifest.v2+json", - } - flagOutput = &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "write signature to a specific path", - } - flagLocal = &cli.BoolFlag{ - Name: "local", - Aliases: []string{"l"}, - Usage: "reference is a local file", - } - flagSignature = &cli.StringSliceFlag{ + flagUsername = &pflag.Flag{ + Name: "username", + Shorthand: "u", + Usage: "Username for registry operations (default from $NOTATION_USERNAME)", + } + setflagUsername = func(fs *pflag.FlagSet, p *string) { + fs.StringVarP(p, flagUsername.Name, flagUsername.Shorthand, "", flagUsername.Usage) + } + + flagPassword = &pflag.Flag{ + Name: "password", + Shorthand: "p", + Usage: "Password for registry operations (default from $NOTATION_PASSWORD)", + } + setFlagPassword = func(fs *pflag.FlagSet, p *string) { + fs.StringVarP(p, flagPassword.Name, flagPassword.Shorthand, "", flagPassword.Usage) + } + + flagPlainHTTP = &pflag.Flag{ + Name: "plain-http", + Usage: "Registry access via plain HTTP", + DefValue: "false", + } + setFlagPlainHTTP = func(fs *pflag.FlagSet, p *bool) { + fs.BoolVar(p, flagPlainHTTP.Name, false, flagPlainHTTP.Usage) + } + + flagMediaType = &pflag.Flag{ + Name: "media-type", + Usage: "specify the media type of the manifest read from file or stdin", + DefValue: defaultMediaType, + } + setFlagMediaType = func(fs *pflag.FlagSet, p *string) { + fs.StringVar(p, flagMediaType.Name, defaultMediaType, flagMediaType.Usage) + } + + flagOutput = &pflag.Flag{ + Name: "output", + Shorthand: "o", + Usage: "write signature to a specific path", + } + setFlagOutput = func(fs *pflag.FlagSet, p *string) { + fs.StringVarP(p, flagOutput.Name, flagOutput.Shorthand, "", flagOutput.Usage) + } + + flagLocal = &pflag.Flag{ + Name: "local", + Shorthand: "l", + Usage: "reference is a local file", + DefValue: "false", + } + setFlagLocal = func(fs *pflag.FlagSet, p *bool) { + fs.BoolVarP(p, flagLocal.Name, flagLocal.Shorthand, false, flagLocal.Usage) + } + + flagSignature = &pflag.Flag{ Name: "signature", - Aliases: []string{"s", "f"}, + Shorthand: "s", Usage: "signature files", - TakesFile: true, + } + setFlagSignature = func(fs *pflag.FlagSet, p *[]string) { + fs.StringSliceVarP(p, flagSignature.Name, flagSignature.Shorthand, []string{}, flagSignature.Usage) } ) + +type SecureFlagOpts struct { + Username string + Password string + PlainHTTP bool +} + +// ApplyFlags set flags and their default values for the FlagSet +func (opts *SecureFlagOpts) ApplyFlags(fs *pflag.FlagSet) { + setflagUsername(fs, &opts.Username) + setFlagPassword(fs, &opts.Password) + setFlagPlainHTTP(fs, &opts.PlainHTTP) + opts.Username = os.Getenv(defaultUsernameEnv) + opts.Password = os.Getenv(defaultPasswordEnv) +} + +type CommonFlagOpts struct { + Local bool + MediaType string +} + +// ApplyFlags set flags and their default values for the FlagSet +func (opts *CommonFlagOpts) ApplyFlags(fs *pflag.FlagSet) { + setFlagMediaType(fs, &opts.MediaType) + setFlagLocal(fs, &opts.Local) +} + +type RemoteFlagOpts struct { + SecureFlagOpts + CommonFlagOpts +} + +// ApplyFlags set flags and their default values for the FlagSet +func (opts *RemoteFlagOpts) ApplyFlags(fs *pflag.FlagSet) { + opts.SecureFlagOpts.ApplyFlags(fs) + opts.CommonFlagOpts.ApplyFlags(fs) +} diff --git a/cmd/notation/key.go b/cmd/notation/key.go index dfcdd548f..8d5c7ce7c 100644 --- a/cmd/notation/key.go +++ b/cmd/notation/key.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/tls" "errors" "fmt" @@ -12,98 +13,152 @@ import ( "github.com/notaryproject/notation/internal/ioutil" "github.com/notaryproject/notation/internal/slices" "github.com/notaryproject/notation/pkg/config" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( - keyCommand = &cli.Command{ - Name: "key", - Usage: "Manage keys used for signing", - Subcommands: []*cli.Command{ - keyAddCommand, - keyUpdateCommand, - keyListCommand, - keyRemoveCommand, - }, + keyDefaultFlag = &pflag.Flag{ + Name: "default", + Shorthand: "d", + Usage: "mark as default", + } + setKeyDefaultFlag = func(fs *pflag.FlagSet, p *bool) { + fs.BoolVarP(p, keyDefaultFlag.Name, keyDefaultFlag.Shorthand, false, keyDefaultFlag.Usage) } +) + +type keyAddOpts struct { + name string + plugin string + id string + pluginConfig string + isDefault bool + keyPath string + certPath string +} + +type keyUpdateOpts struct { + name string + isDefault bool +} - keyDefaultFlag = &cli.BoolFlag{ - Name: "default", - Aliases: []string{"d"}, - Usage: "mark as default", +type keyRemoveOpts struct { + names []string +} + +func keyCommand() *cobra.Command { + command := &cobra.Command{ + Use: "key", + Short: "Manage keys used for signing", } + command.AddCommand(keyAddCommand(nil), keyUpdateCommand(nil), keyListCommand(), keyRemoveCommand(nil)) + return command +} - keyAddCommand = &cli.Command{ - Name: "add", - Usage: "Add key to signing key list", - ArgsUsage: "[ ]", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "key name", - Required: true, - }, - &cli.StringFlag{ - Name: "plugin", - Aliases: []string{"p"}, - Usage: "signing plugin name", - }, - &cli.StringFlag{ - Name: "id", - Usage: "key id (required if --plugin is set)", - }, - cmd.FlagPluginConfig, - keyDefaultFlag, +func keyAddCommand(opts *keyAddOpts) *cobra.Command { + if opts == nil { + opts = &keyAddOpts{} + } + command := &cobra.Command{ + Use: "add [key_path cert_path]", + Short: "Add key to signing key list", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) >= 2 { + opts.keyPath = args[0] + opts.certPath = args[1] + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return addKey(cmd, opts) }, - Action: addKey, } + command.Flags().StringVarP(&opts.name, "name", "n", "", "key name") + command.MarkFlagRequired("name") + + command.Flags().StringVarP(&opts.plugin, "plugin", "p", "", "signing plugin name") + command.Flags().StringVar(&opts.id, "id", "", "key id (required if --plugin is set)") - keyUpdateCommand = &cli.Command{ - Name: "update", - Usage: "Update key in signing key list", - Aliases: []string{"set"}, - ArgsUsage: "", - Flags: []cli.Flag{ - keyDefaultFlag, + cmd.SetPflagPluginConfig(command.Flags(), &opts.pluginConfig) + setKeyDefaultFlag(command.Flags(), &opts.isDefault) + return command +} + +func keyUpdateCommand(opts *keyUpdateOpts) *cobra.Command { + if opts == nil { + opts = &keyUpdateOpts{} + } + command := &cobra.Command{ + Use: "update [name]", + Aliases: []string{"set"}, + Short: "Update key in signing key list", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing key name") + } + opts.name = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return updateKey(opts) }, - Action: updateKey, } - keyListCommand = &cli.Command{ - Name: "list", - Usage: "List keys used for signing", + setKeyDefaultFlag(command.Flags(), &opts.isDefault) + return command +} + +func keyListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", Aliases: []string{"ls"}, - Action: listKeys, + Short: "List keys used for signing", + RunE: func(cmd *cobra.Command, args []string) error { + return listKeys() + }, } +} - keyRemoveCommand = &cli.Command{ - Name: "remove", - Usage: "Remove key from signing key list", - Aliases: []string{"rm"}, - ArgsUsage: "[name] ...", - Action: removeKeys, +func keyRemoveCommand(opts *keyRemoveOpts) *cobra.Command { + if opts == nil { + opts = &keyRemoveOpts{} + } + return &cobra.Command{ + Use: "remove [name]...", + Aliases: []string{"rm"}, + Short: "Remove key from signing key list", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing key names") + } + opts.names = args + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return removeKeys(opts) + }, } -) +} -func addKey(ctx *cli.Context) error { +func addKey(command *cobra.Command, opts *keyAddOpts) error { cfg, err := config.LoadOrDefault() if err != nil { return err } var key config.KeySuite - pluginName := ctx.String("plugin") - name := ctx.String("name") + pluginName := opts.plugin + name := opts.name if pluginName != "" { - key, err = addExternalKey(ctx, pluginName, name) + key, err = addExternalKey(command.Context(), opts, pluginName, name) } else { - key, err = newX509KeyPair(ctx, name) + key, err = newX509KeyPair(opts, name) } if err != nil { return err } - isDefault := ctx.Bool(keyDefaultFlag.Name) + isDefault := opts.isDefault err = addKeyCore(cfg, key, isDefault) if err != nil { return err @@ -122,20 +177,20 @@ func addKey(ctx *cli.Context) error { return nil } -func addExternalKey(ctx *cli.Context, pluginName, keyName string) (config.KeySuite, error) { - id := ctx.String("id") +func addExternalKey(ctx context.Context, opts *keyAddOpts, pluginName, keyName string) (config.KeySuite, error) { + id := opts.id if id == "" { return config.KeySuite{}, errors.New("missing key id") } mgr := manager.New(config.PluginDirPath) - p, err := mgr.Get(ctx.Context, pluginName) + p, err := mgr.Get(ctx, pluginName) if err != nil { return config.KeySuite{}, err } if p.Err != nil { return config.KeySuite{}, fmt.Errorf("invalid plugin: %w", p.Err) } - pluginConfig, err := cmd.ParseFlagPluginConfig(ctx) + pluginConfig, err := cmd.ParseFlagPluginConfig(opts.pluginConfig) if err != nil { return config.KeySuite{}, err } @@ -149,20 +204,19 @@ func addExternalKey(ctx *cli.Context, pluginName, keyName string) (config.KeySui }, nil } -func newX509KeyPair(ctx *cli.Context, keyName string) (config.KeySuite, error) { - args := ctx.Args() - switch args.Len() { - case 0: +func newX509KeyPair(opts *keyAddOpts, keyName string) (config.KeySuite, error) { + if opts.keyPath == "" { return config.KeySuite{}, errors.New("missing key and certificate paths") - case 1: + } + if opts.certPath == "" { return config.KeySuite{}, errors.New("missing certificate path for the corresponding key") } - keyPath, err := filepath.Abs(args.Get(0)) + keyPath, err := filepath.Abs(opts.keyPath) if err != nil { return config.KeySuite{}, err } - certPath, err := filepath.Abs(args.Get(1)) + certPath, err := filepath.Abs(opts.certPath) if err != nil { return config.KeySuite{}, err } @@ -188,13 +242,9 @@ func addKeyCore(cfg *config.File, key config.KeySuite, markDefault bool) error { return nil } -func updateKey(ctx *cli.Context) error { +func updateKey(opts *keyUpdateOpts) error { // initialize - name := ctx.Args().First() - if name == "" { - return errors.New("missing key name") - } - + name := opts.name // core process cfg, err := config.LoadOrDefault() if err != nil { @@ -203,7 +253,7 @@ func updateKey(ctx *cli.Context) error { if !slices.Contains(cfg.SigningKeys.Keys, name) { return errors.New(name + ": not found") } - if !ctx.Bool(keyDefaultFlag.Name) { + if !opts.isDefault { return nil } if cfg.SigningKeys.Default != name { @@ -218,7 +268,7 @@ func updateKey(ctx *cli.Context) error { return nil } -func listKeys(ctx *cli.Context) error { +func listKeys() error { // core process cfg, err := config.LoadOrDefault() if err != nil { @@ -229,13 +279,7 @@ func listKeys(ctx *cli.Context) error { return ioutil.PrintKeyMap(os.Stdout, cfg.SigningKeys.Default, cfg.SigningKeys.Keys) } -func removeKeys(ctx *cli.Context) error { - // initialize - names := ctx.Args().Slice() - if len(names) == 0 { - return errors.New("missing key names") - } - +func removeKeys(opts *keyRemoveOpts) error { // core process cfg, err := config.LoadOrDefault() if err != nil { @@ -244,7 +288,7 @@ func removeKeys(ctx *cli.Context) error { prevDefault := cfg.SigningKeys.Default var removedNames []string - for _, name := range names { + for _, name := range opts.names { idx := slices.Index(cfg.SigningKeys.Keys, name) if idx < 0 { return errors.New(name + ": not found") diff --git a/cmd/notation/list.go b/cmd/notation/list.go index 05400e1c6..66a3a78aa 100644 --- a/cmd/notation/list.go +++ b/cmd/notation/list.go @@ -4,40 +4,52 @@ import ( "errors" "fmt" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) -var listCommand = &cli.Command{ - Name: "list", - Usage: "List signatures from remote", - Aliases: []string{"ls"}, - ArgsUsage: "", - Flags: []cli.Flag{ - flagUsername, - flagPassword, - }, - Action: runList, +type listOpts struct { + SecureFlagOpts + reference string } -func runList(ctx *cli.Context) error { - // initialize - if !ctx.Args().Present() { - return errors.New("no reference specified") +func listCommand(opts *listOpts) *cobra.Command { + if opts == nil { + opts = &listOpts{} + } + cmd := &cobra.Command{ + Use: "list [reference]", + Aliases: []string{"ls"}, + Short: "List signatures from remote", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no reference specified") + } + opts.reference = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(cmd, opts) + }, } + opts.ApplyFlags(cmd.Flags()) + return cmd +} - reference := ctx.Args().First() - sigRepo, err := getSignatureRepository(ctx, reference) +func runList(command *cobra.Command, opts *listOpts) error { + // initialize + reference := opts.reference + sigRepo, err := getSignatureRepository(&opts.SecureFlagOpts, reference) if err != nil { return err } // core process - manifestDesc, err := getManifestDescriptorFromReference(ctx, reference) + manifestDesc, err := getManifestDescriptorFromReference(command.Context(), &opts.SecureFlagOpts, reference) if err != nil { return err } - sigManifests, err := sigRepo.ListSignatureManifests(ctx.Context, manifestDesc.Digest) + sigManifests, err := sigRepo.ListSignatureManifests(command.Context(), manifestDesc.Digest) if err != nil { return fmt.Errorf("lookup signature failure: %v", err) } diff --git a/cmd/notation/login.go b/cmd/notation/login.go index 3d1329817..71c09157a 100644 --- a/cmd/notation/login.go +++ b/cmd/notation/login.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "io" @@ -8,41 +9,57 @@ import ( "strings" "github.com/notaryproject/notation/pkg/auth" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" orasauth "oras.land/oras-go/v2/registry/remote/auth" ) -var loginCommand = &cli.Command{ - Name: "login", - Usage: "Provides credentials for authenticated registry operations", - UsageText: `notation login [options] [server] - +type loginOpts struct { + SecureFlagOpts + passwordStdin bool + server string +} + +func loginCommand(opts *loginOpts) *cobra.Command { + if opts == nil { + opts = &loginOpts{} + } + command := &cobra.Command{ + Use: "login [options] [server]", + Short: "Provides credentials for authenticated registry operations", + Long: `notation login [options] [server] + Example - Login with provided username and password: notation login -u -p registry.example.com Example - Login using $NOTATION_USERNAME $NOTATION_PASSWORD variables: notation login registry.example.com`, - ArgsUsage: "[server]", - Flags: []cli.Flag{ - flagUsername, - flagPassword, - &cli.BoolFlag{ - Name: "password-stdin", - Usage: "Take the password from stdin", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no hostname specified") + } + opts.server = args[0] + return nil }, - }, - Before: readPassword, - Action: runLogin, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := readPassword(opts); err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runLogin(cmd, opts) + }, + } + command.Flags().BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the password from stdin") + opts.ApplyFlags(command.Flags()) + return command } -func runLogin(ctx *cli.Context) error { +func runLogin(cmd *cobra.Command, opts *loginOpts) error { // initialize - if !ctx.Args().Present() { - return errors.New("no hostname specified") - } - serverAddress := ctx.Args().First() + serverAddress := opts.server - if err := validateAuthConfig(ctx, serverAddress); err != nil { + if err := validateAuthConfig(cmd.Context(), opts, serverAddress); err != nil { return err } @@ -52,8 +69,8 @@ func runLogin(ctx *cli.Context) error { } // init creds creds := newCredentialFromInput( - ctx.String(flagUsername.Name), - ctx.String(flagPassword.Name), + opts.Username, + opts.Password, ) if err = nativeStore.Store(serverAddress, creds); err != nil { return fmt.Errorf("failed to store credentials: %v", err) @@ -61,12 +78,12 @@ func runLogin(ctx *cli.Context) error { return nil } -func validateAuthConfig(ctx *cli.Context, serverAddress string) error { - registry, err := getRegistryClient(ctx, serverAddress) +func validateAuthConfig(ctx context.Context, opts *loginOpts, serverAddress string) error { + registry, err := getRegistryClient(&opts.SecureFlagOpts, serverAddress) if err != nil { return err } - return registry.Ping(ctx.Context) + return registry.Ping(ctx) } func newCredentialFromInput(username, password string) orasauth.Credential { @@ -80,13 +97,13 @@ func newCredentialFromInput(username, password string) orasauth.Credential { return c } -func readPassword(ctx *cli.Context) error { - if ctx.Bool("password-stdin") { +func readPassword(opts *loginOpts) error { + if opts.passwordStdin { password, err := readLine() if err != nil { return err } - ctx.Set(flagPassword.Name, password) + opts.Password = password } return nil } diff --git a/cmd/notation/logout.go b/cmd/notation/logout.go index 5eb707210..b6eeb20c8 100644 --- a/cmd/notation/logout.go +++ b/cmd/notation/logout.go @@ -4,22 +4,36 @@ import ( "errors" "github.com/notaryproject/notation/pkg/auth" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) -var logoutCommand = &cli.Command{ - Name: "logout", - Usage: "Log out the specified registry hostname", - ArgsUsage: "[server]", - Action: runLogout, +type logoutOpts struct { + server string } -func runLogout(ctx *cli.Context) error { - // initialize - if !ctx.Args().Present() { - return errors.New("no hostname specified") +func logoutCommand(opts *logoutOpts) *cobra.Command { + if opts == nil { + opts = &logoutOpts{} + } + return &cobra.Command{ + Use: "logout [server]", + Short: "Log out the specified registry hostname", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no hostname specified") + } + opts.server = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runLogout(cmd, opts) + }, } - serverAddress := ctx.Args().First() +} + +func runLogout(cmd *cobra.Command, opts *logoutOpts) error { + // initialize + serverAddress := opts.server nativeStore, err := auth.GetCredentialsStore(serverAddress) if err != nil { return err diff --git a/cmd/notation/main.go b/cmd/notation/main.go index fd2bea3a4..bddc03568 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -2,40 +2,32 @@ package main import ( "log" - "os" "github.com/notaryproject/notation/internal/version" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) func main() { - app := &cli.App{ - Name: "notation", - Usage: "Notation - Notary V2", - Version: version.GetVersion(), - Authors: []*cli.Author{ - { - Name: "CNCF Notary Project", - }, - }, - Flags: []cli.Flag{ - flagPlainHTTP, - }, - Commands: []*cli.Command{ - signCommand, - verifyCommand, - pushCommand, - pullCommand, - listCommand, - certCommand, - keyCommand, - cacheCommand, - pluginCommand, - loginCommand, - logoutCommand, - }, + cmd := &cobra.Command{ + Use: "notation", + Short: "Notation - Notary V2", + Version: version.GetVersion(), + SilenceUsage: true, } - if err := app.Run(os.Args); err != nil { + cmd.AddCommand( + signCommand(nil), + verifyCommand(nil), + pushCommand(nil), + pullCommand(nil), + listCommand(nil), + certCommand(), + keyCommand(), + cacheCommand(), + pluginCommand(), + loginCommand(nil), + logoutCommand(nil)) + cmd.PersistentFlags().Bool(flagPlainHTTP.Name, false, flagPlainHTTP.Usage) + if err := cmd.Execute(); err != nil { log.Fatal(err) } } diff --git a/cmd/notation/manifest.go b/cmd/notation/manifest.go index cce70cb87..f701b2b8d 100644 --- a/cmd/notation/manifest.go +++ b/cmd/notation/manifest.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "io" "math" @@ -8,40 +9,38 @@ import ( "github.com/notaryproject/notation-go" "github.com/opencontainers/go-digest" - "github.com/urfave/cli/v2" "oras.land/oras-go/v2/registry" ) -func getManifestDescriptorFromContext(ctx *cli.Context) (notation.Descriptor, error) { - ref := ctx.Args().First() +func getManifestDescriptorFromContext(ctx context.Context, opts *RemoteFlagOpts, ref string) (notation.Descriptor, error) { if ref == "" { return notation.Descriptor{}, errors.New("missing reference") } - return getManifestDescriptorFromContextWithReference(ctx, ref) + return getManifestDescriptorFromContextWithReference(ctx, opts, ref) } -func getManifestDescriptorFromContextWithReference(ctx *cli.Context, ref string) (notation.Descriptor, error) { - if ctx.Bool(flagLocal.Name) { - mediaType := ctx.String(flagMediaType.Name) +func getManifestDescriptorFromContextWithReference(ctx context.Context, opts *RemoteFlagOpts, ref string) (notation.Descriptor, error) { + if opts.Local { + mediaType := opts.MediaType if ref == "-" { return getManifestDescriptorFromReader(os.Stdin, mediaType) } return getManifestDescriptorFromFile(ref, mediaType) } - return getManifestDescriptorFromReference(ctx, ref) + return getManifestDescriptorFromReference(ctx, &opts.SecureFlagOpts, ref) } -func getManifestDescriptorFromReference(ctx *cli.Context, reference string) (notation.Descriptor, error) { +func getManifestDescriptorFromReference(ctx context.Context, opts *SecureFlagOpts, reference string) (notation.Descriptor, error) { ref, err := registry.ParseReference(reference) if err != nil { return notation.Descriptor{}, err } - repo, err := getRepositoryClient(ctx, ref) + repo, err := getRepositoryClient(opts, ref) if err != nil { return notation.Descriptor{}, err } - return repo.Resolve(ctx.Context, ref.ReferenceOrDefault()) + return repo.Resolve(ctx, ref.ReferenceOrDefault()) } func getManifestDescriptorFromFile(path, mediaType string) (notation.Descriptor, error) { diff --git a/cmd/notation/plugin.go b/cmd/notation/plugin.go index 47153ab74..911510101 100644 --- a/cmd/notation/plugin.go +++ b/cmd/notation/plugin.go @@ -6,29 +6,32 @@ import ( "github.com/notaryproject/notation-go/plugin/manager" "github.com/notaryproject/notation/internal/ioutil" "github.com/notaryproject/notation/pkg/config" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) -var ( - pluginCommand = &cli.Command{ - Name: "plugin", - Usage: "Manage plugins", - Subcommands: []*cli.Command{ - pluginListCommand, - }, +func pluginCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "plugin", + Short: "Manage plugins", } + cmd.AddCommand(pluginListCommand()) + return cmd +} - pluginListCommand = &cli.Command{ - Name: "list", - Usage: "List registered plugins", +func pluginListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", Aliases: []string{"ls"}, - Action: listPlugins, + Short: "List registered plugins", + RunE: func(cmd *cobra.Command, args []string) error { + return listPlugins(cmd) + }, } -) +} -func listPlugins(ctx *cli.Context) error { +func listPlugins(command *cobra.Command) error { mgr := manager.New(config.PluginDirPath) - plugins, err := mgr.List(ctx.Context) + plugins, err := mgr.List(command.Context()) if err != nil { return err } diff --git a/cmd/notation/pull.go b/cmd/notation/pull.go index 3d4051dcc..3dc7138a8 100644 --- a/cmd/notation/pull.go +++ b/cmd/notation/pull.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "path/filepath" @@ -10,66 +11,77 @@ import ( "github.com/notaryproject/notation/pkg/cache" "github.com/notaryproject/notation/pkg/config" "github.com/opencontainers/go-digest" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" "oras.land/oras-go/v2/registry" ) -var pullCommand = &cli.Command{ - Name: "pull", - Usage: "Pull signatures from remote", - ArgsUsage: "", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "strict", - Usage: "pull the signature without lookup the manifest", - }, - flagOutput, - flagUsername, - flagPassword, - }, - Action: runPull, +type pullOpts struct { + SecureFlagOpts + strict bool + reference string + output string } -func runPull(ctx *cli.Context) error { - // initialize - if !ctx.Args().Present() { - return errors.New("no reference specified") +func pullCommand(opts *pullOpts) *cobra.Command { + if opts == nil { + opts = &pullOpts{} + } + cmd := &cobra.Command{ + Use: "pull [reference]", + Short: "Pull signatures from remote", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no reference specified") + } + opts.reference = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runPull(cmd, opts) + }, } + cmd.Flags().BoolVar(&opts.strict, "strict", false, "pull the signature without lookup the manifest") + setFlagOutput(cmd.Flags(), &opts.output) + opts.ApplyFlags(cmd.Flags()) + return cmd +} - reference := ctx.Args().First() - sigRepo, err := getSignatureRepository(ctx, reference) +func runPull(command *cobra.Command, opts *pullOpts) error { + // initialize + reference := opts.reference + sigRepo, err := getSignatureRepository(&opts.SecureFlagOpts, reference) if err != nil { return err } // core process - if ctx.Bool("strict") { - return pullSignatureStrict(ctx, sigRepo, reference) + if opts.strict { + return pullSignatureStrict(command.Context(), opts, sigRepo, reference) } - manifestDesc, err := getManifestDescriptorFromReference(ctx, reference) + manifestDesc, err := getManifestDescriptorFromReference(command.Context(), &opts.SecureFlagOpts, reference) if err != nil { return err } - sigManifests, err := sigRepo.ListSignatureManifests(ctx.Context, manifestDesc.Digest) + sigManifests, err := sigRepo.ListSignatureManifests(command.Context(), manifestDesc.Digest) if err != nil { return fmt.Errorf("list signature manifests failure: %v", err) } - path := ctx.String(flagOutput.Name) + path := opts.output for _, sigManifest := range sigManifests { sigDigest := sigManifest.Blob.Digest if path != "" { outputPath := filepath.Join(path, sigDigest.Encoded()+config.SignatureExtension) - sig, err := sigRepo.Get(ctx.Context, sigDigest) + sig, err := sigRepo.Get(command.Context(), sigDigest) if err != nil { return fmt.Errorf("get signature failure: %v: %v", sigDigest, err) } if err := osutil.WriteFile(outputPath, sig); err != nil { return fmt.Errorf("fail to write signature: %v: %v", sigDigest, err) } - } else if err := cache.PullSignature(ctx.Context, sigRepo, manifestDesc.Digest, sigDigest); err != nil { + } else if err := cache.PullSignature(command.Context(), sigRepo, manifestDesc.Digest, sigDigest); err != nil { return err } @@ -80,7 +92,7 @@ func runPull(ctx *cli.Context) error { return nil } -func pullSignatureStrict(ctx *cli.Context, sigRepo notationregistry.SignatureRepository, reference string) error { +func pullSignatureStrict(ctx context.Context, opts *pullOpts, sigRepo notationregistry.SignatureRepository, reference string) error { ref, err := registry.ParseReference(reference) if err != nil { return err @@ -90,11 +102,11 @@ func pullSignatureStrict(ctx *cli.Context, sigRepo notationregistry.SignatureRep return fmt.Errorf("invalid signature digest: %v", err) } - sig, err := sigRepo.Get(ctx.Context, sigDigest) + sig, err := sigRepo.Get(ctx, sigDigest) if err != nil { return fmt.Errorf("get signature failure: %v: %v", sigDigest, err) } - outputPath := ctx.String(flagOutput.Name) + outputPath := opts.output if outputPath == "" { outputPath = sigDigest.Encoded() + config.SignatureExtension } @@ -107,19 +119,18 @@ func pullSignatureStrict(ctx *cli.Context, sigRepo notationregistry.SignatureRep return nil } -func pullSignatures(ctx *cli.Context, manifestDigest digest.Digest) error { - reference := ctx.Args().First() - sigRepo, err := getSignatureRepository(ctx, reference) +func pullSignatures(command *cobra.Command, reference string, opts *SecureFlagOpts, manifestDigest digest.Digest) error { + sigRepo, err := getSignatureRepository(opts, reference) if err != nil { return err } - sigManifests, err := sigRepo.ListSignatureManifests(ctx.Context, manifestDigest) + sigManifests, err := sigRepo.ListSignatureManifests(command.Context(), manifestDigest) if err != nil { return fmt.Errorf("lookup signature failure: %v", err) } for _, sigManifest := range sigManifests { - if err := cache.PullSignature(ctx.Context, sigRepo, manifestDigest, sigManifest.Blob.Digest); err != nil { + if err := cache.PullSignature(command.Context(), sigRepo, manifestDigest, sigManifest.Blob.Digest); err != nil { return err } } diff --git a/cmd/notation/push.go b/cmd/notation/push.go index 1d5eab871..98128937d 100644 --- a/cmd/notation/push.go +++ b/cmd/notation/push.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "os" @@ -8,32 +9,46 @@ import ( "github.com/notaryproject/notation-go" "github.com/notaryproject/notation/pkg/cache" "github.com/notaryproject/notation/pkg/config" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) -var pushCommand = &cli.Command{ - Name: "push", - Usage: "Push signature to remote", - ArgsUsage: "", - Flags: []cli.Flag{ - flagSignature, - flagUsername, - flagPassword, - }, - Action: runPush, +type pushOpts struct { + SecureFlagOpts + reference string + signatures []string } -func runPush(ctx *cli.Context) error { - // initialize - if !ctx.Args().Present() { - return errors.New("no reference specified") +func pushCommand(opts *pushOpts) *cobra.Command { + if opts == nil { + opts = &pushOpts{} + } + cmd := &cobra.Command{ + Use: "push [reference]", + Short: "Push signature to remote", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no reference specified") + } + opts.reference = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runPush(cmd, opts) + }, } - ref := ctx.Args().First() - manifestDesc, err := getManifestDescriptorFromReference(ctx, ref) + setFlagSignature(cmd.Flags(), &opts.signatures) + opts.ApplyFlags(cmd.Flags()) + return cmd +} + +func runPush(command *cobra.Command, opts *pushOpts) error { + // initialize + ref := opts.reference + manifestDesc, err := getManifestDescriptorFromReference(command.Context(), &opts.SecureFlagOpts, ref) if err != nil { return err } - sigPaths := ctx.StringSlice(flagSignature.Name) + sigPaths := opts.signatures if len(sigPaths) == 0 { sigDigests, err := cache.SignatureDigests(manifestDesc.Digest) if err != nil { @@ -45,7 +60,7 @@ func runPush(ctx *cli.Context) error { } // core process - sigRepo, err := getSignatureRepository(ctx, ref) + sigRepo, err := getSignatureRepository(&opts.SecureFlagOpts, ref) if err != nil { return err } @@ -55,7 +70,7 @@ func runPush(ctx *cli.Context) error { return err } // pass in nonempty annotations if needed - sigDesc, _, err := sigRepo.PutSignatureManifest(ctx.Context, sig, manifestDesc, make(map[string]string)) + sigDesc, _, err := sigRepo.PutSignatureManifest(command.Context(), sig, manifestDesc, make(map[string]string)) if err != nil { return fmt.Errorf("put signature manifest failure: %v", err) } @@ -67,20 +82,20 @@ func runPush(ctx *cli.Context) error { return nil } -func pushSignature(ctx *cli.Context, ref string, sig []byte) (notation.Descriptor, error) { +func pushSignature(ctx context.Context, opts *SecureFlagOpts, ref string, sig []byte) (notation.Descriptor, error) { // initialize - sigRepo, err := getSignatureRepository(ctx, ref) + sigRepo, err := getSignatureRepository(opts, ref) if err != nil { return notation.Descriptor{}, err } - manifestDesc, err := getManifestDescriptorFromReference(ctx, ref) + manifestDesc, err := getManifestDescriptorFromReference(ctx, opts, ref) if err != nil { return notation.Descriptor{}, err } // core process // pass in nonempty annotations if needed - sigDesc, _, err := sigRepo.PutSignatureManifest(ctx.Context, sig, manifestDesc, make(map[string]string)) + sigDesc, _, err := sigRepo.PutSignatureManifest(ctx, sig, manifestDesc, make(map[string]string)) if err != nil { return notation.Descriptor{}, fmt.Errorf("put signature manifest failure: %v", err) } diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 551398add..61e0c53ec 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -8,45 +8,45 @@ import ( "github.com/notaryproject/notation/internal/version" loginauth "github.com/notaryproject/notation/pkg/auth" "github.com/notaryproject/notation/pkg/config" - "github.com/urfave/cli/v2" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" ) -func getSignatureRepository(ctx *cli.Context, reference string) (notationregistry.SignatureRepository, error) { +func getSignatureRepository(opts *SecureFlagOpts, reference string) (notationregistry.SignatureRepository, error) { ref, err := registry.ParseReference(reference) if err != nil { return nil, err } - return getRepositoryClient(ctx, ref) + return getRepositoryClient(opts, ref) } -func getRegistryClient(ctx *cli.Context, serverAddress string) (*remote.Registry, error) { +func getRegistryClient(opts *SecureFlagOpts, serverAddress string) (*remote.Registry, error) { reg, err := remote.NewRegistry(serverAddress) if err != nil { return nil, err } - reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, reg.Reference) + reg.Client, reg.PlainHTTP, err = getAuthClient(opts, reg.Reference) if err != nil { return nil, err } return reg, nil } -func getRepositoryClient(ctx *cli.Context, ref registry.Reference) (*notationregistry.RepositoryClient, error) { - authClient, plainHTTP, err := getAuthClient(ctx, ref) +func getRepositoryClient(opts *SecureFlagOpts, ref registry.Reference) (*notationregistry.RepositoryClient, error) { + authClient, plainHTTP, err := getAuthClient(opts, ref) if err != nil { return nil, err } return notationregistry.NewRepositoryClient(authClient, ref, plainHTTP), nil } -func getAuthClient(ctx *cli.Context, ref registry.Reference) (*auth.Client, bool, error) { +func getAuthClient(opts *SecureFlagOpts, ref registry.Reference) (*auth.Client, bool, error) { var plainHTTP bool - if ctx.IsSet(flagPlainHTTP.Name) { - plainHTTP = ctx.Bool(flagPlainHTTP.Name) + + if opts.PlainHTTP { + plainHTTP = opts.PlainHTTP } else { plainHTTP = config.IsRegistryInsecure(ref.Registry) if !plainHTTP { @@ -55,10 +55,9 @@ func getAuthClient(ctx *cli.Context, ref registry.Reference) (*auth.Client, bool } } } - cred := auth.Credential{ - Username: ctx.String(flagUsername.Name), - Password: ctx.String(flagPassword.Name), + Username: opts.Username, + Password: opts.Password, } if cred.Username == "" { cred = auth.Credential{ diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index 47104f218..ae902402c 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -1,7 +1,10 @@ package main import ( + "context" + "errors" "fmt" + "time" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp" @@ -9,58 +12,74 @@ import ( "github.com/notaryproject/notation/internal/osutil" "github.com/notaryproject/notation/pkg/config" "github.com/opencontainers/go-digest" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) -var signCommand = &cli.Command{ - Name: "sign", - Usage: "Signs artifacts", - ArgsUsage: "", - Flags: []cli.Flag{ - cmd.FlagKey, - cmd.FlagKeyFile, - cmd.FlagCertFile, - cmd.FlagTimestamp, - cmd.FlagExpiry, - cmd.FlagReference, - flagLocal, - flagOutput, - &cli.BoolFlag{ - Name: "push", - Usage: "push after successful signing", - Value: true, +type signOpts struct { + cmd.SignerFlagOpts + RemoteFlagOpts + timestamp string + expiry time.Duration + originReference string + output string + push bool + pushReference string + pluginConfig string + reference string +} + +func signCommand(opts *signOpts) *cobra.Command { + if opts == nil { + opts = &signOpts{} + } + command := &cobra.Command{ + Use: "sign [reference]", + Short: "Signs artifacts", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing reference") + } + opts.reference = args[0] + return nil }, - &cli.StringFlag{ - Name: "push-reference", - Usage: "different remote to store signature", + RunE: func(cmd *cobra.Command, args []string) error { + return runSign(cmd, opts) }, - flagUsername, - flagPassword, - flagMediaType, - cmd.FlagPluginConfig, - }, - Action: runSign, + } + opts.SignerFlagOpts.ApplyFlags(command.Flags()) + opts.RemoteFlagOpts.ApplyFlags(command.Flags()) + + cmd.SetPflagTimestamp(command.Flags(), &opts.timestamp) + cmd.SetPflagExpiry(command.Flags(), &opts.expiry) + cmd.SetPflagReference(command.Flags(), &opts.originReference) + setFlagOutput(command.Flags(), &opts.output) + + command.Flags().BoolVar(&opts.push, "push", true, "push after successful signing") + command.Flags().StringVar(&opts.pushReference, "push-reference", "", "different remote to store signature") + + cmd.SetPflagPluginConfig(command.Flags(), &opts.pluginConfig) + return command } -func runSign(ctx *cli.Context) error { +func runSign(command *cobra.Command, cmdOpts *signOpts) error { // initialize - signer, err := cmd.GetSigner(ctx) + signer, err := cmd.GetSigner(&cmdOpts.SignerFlagOpts) if err != nil { return err } // core process - desc, opts, err := prepareSigningContent(ctx) + desc, opts, err := prepareSigningContent(command.Context(), cmdOpts) if err != nil { return err } - sig, err := signer.Sign(ctx.Context, desc, opts) + sig, err := signer.Sign(command.Context(), desc, opts) if err != nil { return err } // write out - path := ctx.String(flagOutput.Name) + path := cmdOpts.output if path == "" { path = config.SignaturePath(digest.Digest(desc.Digest), digest.FromBytes(sig)) } @@ -68,11 +87,11 @@ func runSign(ctx *cli.Context) error { return err } - if ref := ctx.String("push-reference"); ctx.Bool("push") && !(ctx.Bool(flagLocal.Name) && ref == "") { + if ref := cmdOpts.pushReference; cmdOpts.push && !(cmdOpts.Local && ref == "") { if ref == "" { - ref = ctx.Args().First() + ref = cmdOpts.reference } - if _, err := pushSignature(ctx, ref, sig); err != nil { + if _, err := pushSignature(command.Context(), &cmdOpts.SecureFlagOpts, ref, sig); err != nil { return fmt.Errorf("fail to push signature to %q: %v: %v", ref, desc.Digest, @@ -85,28 +104,28 @@ func runSign(ctx *cli.Context) error { return nil } -func prepareSigningContent(ctx *cli.Context) (notation.Descriptor, notation.SignOptions, error) { - manifestDesc, err := getManifestDescriptorFromContext(ctx) +func prepareSigningContent(ctx context.Context, opts *signOpts) (notation.Descriptor, notation.SignOptions, error) { + manifestDesc, err := getManifestDescriptorFromContext(ctx, &opts.RemoteFlagOpts, opts.reference) if err != nil { return notation.Descriptor{}, notation.SignOptions{}, err } - if identity := ctx.String(cmd.FlagReference.Name); identity != "" { + if identity := opts.originReference; identity != "" { manifestDesc.Annotations = map[string]string{ "identity": identity, } } var tsa timestamp.Timestamper - if endpoint := ctx.String(cmd.FlagTimestamp.Name); endpoint != "" { + if endpoint := opts.timestamp; endpoint != "" { if tsa, err = timestamp.NewHTTPTimestamper(nil, endpoint); err != nil { return notation.Descriptor{}, notation.SignOptions{}, err } } - pluginConfig, err := cmd.ParseFlagPluginConfig(ctx) + pluginConfig, err := cmd.ParseFlagPluginConfig(opts.pluginConfig) if err != nil { return notation.Descriptor{}, notation.SignOptions{}, err } return manifestDesc, notation.SignOptions{ - Expiry: cmd.GetExpiry(ctx), + Expiry: cmd.GetExpiry(opts.expiry), TSA: tsa, PluginConfig: pluginConfig, }, nil diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index ad4ad0492..a71733cf4 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -13,53 +13,59 @@ import ( "github.com/notaryproject/notation/pkg/cache" "github.com/notaryproject/notation/pkg/config" "github.com/opencontainers/go-digest" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" ) -var verifyCommand = &cli.Command{ - Name: "verify", - Usage: "Verifies OCI Artifacts", - ArgsUsage: "", - Flags: []cli.Flag{ - flagSignature, - &cli.StringSliceFlag{ - Name: "cert", - Aliases: []string{"c"}, - Usage: "certificate names for verification", - }, - &cli.StringSliceFlag{ - Name: cmd.FlagCertFile.Name, - Usage: "certificate files for verification", - TakesFile: true, +type verifyOpts struct { + RemoteFlagOpts + signatures []string + certs []string + certFiles []string + pull bool + reference string +} + +func verifyCommand(opts *verifyOpts) *cobra.Command { + if opts == nil { + opts = &verifyOpts{} + } + command := &cobra.Command{ + Use: "verify [reference]", + Short: "Verifies OCI Artifacts", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing reference") + } + opts.reference = args[0] + return nil }, - &cli.BoolFlag{ - Name: "pull", - Usage: "pull remote signatures before verification", - Value: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runVerify(cmd, opts) }, - flagLocal, - flagUsername, - flagPassword, - flagMediaType, - }, - Action: runVerify, + } + setFlagSignature(command.Flags(), &opts.signatures) + command.Flags().StringSliceVarP(&opts.certs, "cert", "c", []string{}, "certificate names for verification") + command.Flags().StringSliceVar(&opts.certFiles, cmd.PflagCertFile.Name, []string{}, "certificate files for verification") + command.Flags().BoolVar(&opts.pull, "pull", true, "pull remote signatures before verification") + opts.ApplyFlags(command.Flags()) + return command } -func runVerify(ctx *cli.Context) error { +func runVerify(command *cobra.Command, opts *verifyOpts) error { // initialize - verifier, err := getVerifier(ctx) + verifier, err := getVerifier(opts) if err != nil { return err } - manifestDesc, err := getManifestDescriptorFromContext(ctx) + manifestDesc, err := getManifestDescriptorFromContext(command.Context(), &opts.RemoteFlagOpts, opts.reference) if err != nil { return err } - sigPaths := ctx.StringSlice(flagSignature.Name) + sigPaths := opts.signatures if len(sigPaths) == 0 { - if !ctx.Bool(flagLocal.Name) && ctx.Bool("pull") { - if err := pullSignatures(ctx, digest.Digest(manifestDesc.Digest)); err != nil { + if !opts.Local && opts.pull { + if err := pullSignatures(command, opts.reference, &opts.SecureFlagOpts, digest.Digest(manifestDesc.Digest)); err != nil { return err } } @@ -74,7 +80,7 @@ func runVerify(ctx *cli.Context) error { } // core process - if err := verifySignatures(ctx.Context, verifier, manifestDesc, sigPaths); err != nil { + if err := verifySignatures(command.Context(), verifier, manifestDesc, sigPaths); err != nil { return err } @@ -110,9 +116,8 @@ func verifySignatures(ctx context.Context, verifier notation.Verifier, manifestD return lastErr } -func getVerifier(ctx *cli.Context) (notation.Verifier, error) { - certPaths := ctx.StringSlice(cmd.FlagCertFile.Name) - certPaths, err := appendCertPathFromName(certPaths, ctx.StringSlice("cert")) +func getVerifier(opts *verifyOpts) (notation.Verifier, error) { + certPaths, err := appendCertPathFromName(opts.certFiles, opts.certs) if err != nil { return nil, err } diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index 9381e8db1..475dc4be4 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/urfave/cli/v2" ) @@ -62,24 +61,24 @@ var ( Shorthand: "k", Usage: "signing key name", } - SetPflagKey = func(cmd *cobra.Command, p *string) { - cmd.Flags().StringVarP(p, PflagKey.Name, PflagKey.Shorthand, "", PflagKey.Usage) + SetPflagKey = func(fs *pflag.FlagSet, p *string) { + fs.StringVarP(p, PflagKey.Name, PflagKey.Shorthand, "", PflagKey.Usage) } PflagKeyFile = &pflag.Flag{ Name: "key-file", Usage: "signing key file", } - SetPflagKeyFile = func(cmd *cobra.Command, p *string) { - cmd.Flags().StringVar(p, PflagKeyFile.Name, "", PflagKeyFile.Usage) + SetPflagKeyFile = func(fs *pflag.FlagSet, p *string) { + fs.StringVar(p, PflagKeyFile.Name, "", PflagKeyFile.Usage) } PflagCertFile = &pflag.Flag{ Name: "cert-file", Usage: "signing certificate file", } - SetPflagCertFile = func(cmd *cobra.Command, p *string) { - cmd.Flags().StringVar(p, PflagCertFile.Name, "", PflagCertFile.Usage) + SetPflagCertFile = func(fs *pflag.FlagSet, p *string) { + fs.StringVar(p, PflagCertFile.Name, "", PflagCertFile.Usage) } PflagTimestamp = &pflag.Flag{ @@ -87,8 +86,8 @@ var ( Shorthand: "t", Usage: "timestamp the signed signature via the remote TSA", } - SetPflagTimestamp = func(cmd *cobra.Command, p *string) { - cmd.Flags().StringVarP(p, PflagTimestamp.Name, PflagTimestamp.Shorthand, "", PflagTimestamp.Usage) + SetPflagTimestamp = func(fs *pflag.FlagSet, p *string) { + fs.StringVarP(p, PflagTimestamp.Name, PflagTimestamp.Shorthand, "", PflagTimestamp.Usage) } PflagExpiry = &pflag.Flag{ @@ -96,8 +95,8 @@ var ( Shorthand: "e", Usage: "expire duration", } - SetPflagExpiry = func(cmd *cobra.Command, p *time.Duration) { - cmd.Flags().DurationVarP(p, PflagExpiry.Name, PflagExpiry.Shorthand, time.Duration(0), PflagExpiry.Usage) + SetPflagExpiry = func(fs *pflag.FlagSet, p *time.Duration) { + fs.DurationVarP(p, PflagExpiry.Name, PflagExpiry.Shorthand, time.Duration(0), PflagExpiry.Usage) } PflagReference = &pflag.Flag{ @@ -105,18 +104,17 @@ var ( Shorthand: "r", Usage: "original reference", } - SetPflagReference = func(cmd *cobra.Command, p *string) { - cmd.Flags().StringVarP(p, PflagReference.Name, PflagReference.Shorthand, "", PflagReference.Usage) + SetPflagReference = func(fs *pflag.FlagSet, p *string) { + fs.StringVarP(p, PflagReference.Name, PflagReference.Shorthand, "", PflagReference.Usage) } - // TODO: cobra does not support letter shorthand PflagPluginConfig = &pflag.Flag{ Name: "pluginConfig", Shorthand: "c", Usage: "list of comma-separated {key}={value} pairs that are passed as is to the plugin, refer plugin documentation to set appropriate values", } - SetPflagPluginConfig = func(cmd *cobra.Command, p *string) { - cmd.Flags().StringVarP(p, PflagPluginConfig.Name, PflagPluginConfig.Shorthand, "", PflagPluginConfig.Usage) + SetPflagPluginConfig = func(fs *pflag.FlagSet, p *string) { + fs.StringVarP(p, PflagPluginConfig.Name, PflagPluginConfig.Shorthand, "", PflagPluginConfig.Usage) } ) @@ -126,11 +124,10 @@ type KeyValueSlice interface { String() string } -func ParseFlagPluginConfig(ctx *cli.Context) (map[string]string, error) { - val := ctx.String(FlagPluginConfig.Name) - pluginConfig, err := ParseKeyValueListFlag(val) +func ParseFlagPluginConfig(config string) (map[string]string, error) { + pluginConfig, err := ParseKeyValueListFlag(config) if err != nil { - return nil, fmt.Errorf("could not parse %q as value for flag %s: %s", val, FlagPluginConfig.Name, err) + return nil, fmt.Errorf("could not parse %q as value for flag %s: %s", pluginConfig, FlagPluginConfig.Name, err) } return pluginConfig, nil } diff --git a/internal/cmd/options.go b/internal/cmd/options.go new file mode 100644 index 000000000..f48a7273c --- /dev/null +++ b/internal/cmd/options.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/spf13/pflag" +) + +// SignerFlagOpts cmd opts for using cmd.GetSigner +type SignerFlagOpts struct { + Key string + KeyFile string + CertFile string +} + +// ApplyFlags set flags and their default values for the FlagSet +func (opts *SignerFlagOpts) ApplyFlags(fs *pflag.FlagSet) { + SetPflagKey(fs, &opts.Key) + SetPflagKeyFile(fs, &opts.KeyFile) + SetPflagCertFile(fs, &opts.CertFile) +} diff --git a/internal/cmd/signer.go b/internal/cmd/signer.go index 5be9dece2..7c8b63a8f 100644 --- a/internal/cmd/signer.go +++ b/internal/cmd/signer.go @@ -8,19 +8,18 @@ import ( "github.com/notaryproject/notation-go/plugin/manager" "github.com/notaryproject/notation-go/signature" "github.com/notaryproject/notation/pkg/config" - "github.com/urfave/cli/v2" ) // GetSigner returns a signer according to the CLI context. -func GetSigner(ctx *cli.Context) (notation.Signer, error) { +func GetSigner(opts *SignerFlagOpts) (notation.Signer, error) { // Construct a signer from key and cert file if provided as CLI arguments - if keyPath := ctx.String(FlagKeyFile.Name); keyPath != "" { - certPath := ctx.String(FlagCertFile.Name) + if keyPath := opts.KeyFile; keyPath != "" { + certPath := opts.CertFile return signature.NewSignerFromFiles(keyPath, certPath) } // Construct a signer from preconfigured key pair in config.json // if key name is provided as the CLI argument - key, err := config.ResolveKey(ctx.String(FlagKey.Name)) + key, err := config.ResolveKey(opts.Key) if err != nil { return nil, err } @@ -41,8 +40,7 @@ func GetSigner(ctx *cli.Context) (notation.Signer, error) { } // GetExpiry returns the signature expiry according to the CLI context. -func GetExpiry(ctx *cli.Context) time.Time { - expiry := ctx.Duration(FlagExpiry.Name) +func GetExpiry(expiry time.Duration) time.Time { if expiry == 0 { return time.Time{} }