From f7674ca48a5f5d25d42b5a1f0e58bcabca34c749 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Sun, 12 Oct 2025 02:34:01 +0700 Subject: [PATCH 1/7] feat: migrate urfave/cli to v3 Signed-off-by: Hoang Nguyen --- cmd/sops/common/common.go | 8 +- cmd/sops/completion.go | 59 - cmd/sops/main.go | 3688 +++++++++++++++++++------------------ go.mod | 4 +- go.sum | 13 +- version/version.go | 8 +- 6 files changed, 1856 insertions(+), 1924 deletions(-) delete mode 100644 cmd/sops/completion.go diff --git a/cmd/sops/common/common.go b/cmd/sops/common/common.go index 6d6fa0751a..6a2c05a3ec 100644 --- a/cmd/sops/common/common.go +++ b/cmd/sops/common/common.go @@ -21,7 +21,7 @@ import ( "github.com/getsops/sops/v3/stores/yaml" "github.com/getsops/sops/v3/version" "github.com/mitchellh/go-wordwrap" - "github.com/urfave/cli" + "github.com/urfave/cli/v3" "golang.org/x/term" ) @@ -160,13 +160,13 @@ func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops return LoadEncryptedFileEx(loader, inputPath, false) } -// NewExitError returns a cli.ExitError given an error (wrapped in a generic interface{}) +// NewExitError returns a cli.ExitCoder given an error (wrapped in a generic interface{}) // and an exit code to represent the failure -func NewExitError(i interface{}, exitCode int) *cli.ExitError { +func NewExitError(i interface{}, exitCode int) cli.ExitCoder { if userErr, ok := i.(sops.UserError); ok { return NewExitError(userErr.UserError(), exitCode) } - return cli.NewExitError(i, exitCode) + return cli.Exit(i, exitCode) } // StoreForFormat returns the correct format-specific implementation diff --git a/cmd/sops/completion.go b/cmd/sops/completion.go deleted file mode 100644 index 4454026527..0000000000 --- a/cmd/sops/completion.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import "fmt" - -// https://github.com/urfave/cli/blob/v1-maint/autocomplete/zsh_autocomplete -var Zshcompletion = ` -#compdef %s - -_cli_zsh_autocomplete() { - - local -a opts - local cur - cur=${words[-1]} - if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") - else - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") - fi - - if [[ "${opts[1]}" != "" ]]; then - _describe 'values' opts - else - _files - fi - - return -} - -compdef _cli_zsh_autocomplete %s -` - -// https://github.com/urfave/cli/blob/v1-maint/autocomplete/bash_autocomplete -var Bashcompletion = ` -#! /bin/bash - -_cli_bash_autocomplete() { - if [[ "${COMP_WORDS[0]}" != "source" ]]; then - local cur opts base - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - if [[ "$cur" == "-"* ]]; then - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) - else - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) - fi - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi -} -complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete %s -` - -func GenBashCompletion(name string) string { - return fmt.Sprintf(Bashcompletion, name) -} - -func GenZshCompletion(name string) string { - return fmt.Sprintf(Zshcompletion, name, name) -} diff --git a/cmd/sops/main.go b/cmd/sops/main.go index fca10f3032..d96c643f67 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -15,7 +15,7 @@ import ( "strings" "github.com/sirupsen/logrus" - "github.com/urfave/cli" + "github.com/urfave/cli/v3" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -59,11 +59,11 @@ func init() { log = logging.NewLogger("CMD") } -func warnMoreThanOnePositionalArgument(c *cli.Context) { +func warnMoreThanOnePositionalArgument(c *cli.Command) { if c.NArg() > 1 { log.Warn("More than one positional argument provided. Only the first one will be used!") potentialFlag := "" - for i, value := range c.Args() { + for i, value := range c.Args().Slice() { if i > 0 && strings.HasPrefix(value, "-") { potentialFlag = value } @@ -76,1181 +76,1358 @@ func warnMoreThanOnePositionalArgument(c *cli.Context) { func main() { cli.VersionPrinter = version.PrintVersion - app := cli.NewApp() - keyserviceFlags := []cli.Flag{ - cli.BoolTFlag{ - Name: "enable-local-keyservice", - Usage: "use local key service", - EnvVar: "SOPS_ENABLE_LOCAL_KEYSERVICE", - }, - cli.StringSliceFlag{ - Name: "keyservice", - Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000", - EnvVar: "SOPS_KEYSERVICE", - }, - } - app.Name = "sops" - app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, HuaweiCloud KMS, Azure Key Vault, age, and GPG support" - app.ArgsUsage = "sops [options] file" - app.Version = version.Version - app.Authors = []cli.Author{ - {Name: "CNCF Maintainers"}, - } - app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, HuaweiCloud KMS, AZKV, - PGP, and Age - - To encrypt or decrypt a document with AWS KMS, specify the KMS ARN - in the -k flag or in the SOPS_KMS_ARN environment variable. - (you need valid credentials in ~/.aws/credentials or in your env) - - To encrypt or decrypt a document with GCP KMS, specify the - GCP KMS resource ID in the --gcp-kms flag or in the SOPS_GCP_KMS_IDS - environment variable. - (You need to setup Google application default credentials. See - https://developers.google.com/identity/protocols/application-default-credentials) - - To encrypt or decrypt a document with HuaweiCloud KMS, specify the - HuaweiCloud KMS key ID (format: region:key-uuid) in the --hckms flag or in the - SOPS_HUAWEICLOUD_KMS_IDS environment variable. - (You need to setup HuaweiCloud credentials via environment variables: - HUAWEICLOUD_SDK_AK, HUAWEICLOUD_SDK_SK, HUAWEICLOUD_SDK_PROJECT_ID, or - use credentials file at ~/.huaweicloud/credentials) - - To encrypt or decrypt a document with HashiCorp Vault's Transit Secret - Engine, specify the Vault key URI name in the --hc-vault-transit flag - or in the SOPS_VAULT_URIS environment variable (for example - https://vault.example.org:8200/v1/transit/keys/dev, where - 'https://vault.example.org:8200' is the vault server, 'transit' the - enginePath, and 'dev' is the name of the key). - (You need to enable the Transit Secrets Engine in Vault. See - https://www.vaultproject.io/docs/secrets/transit/index.html) - - To encrypt or decrypt a document with Azure Key Vault, specify the - Azure Key Vault key URL in the --azure-kv flag or in the - SOPS_AZURE_KEYVAULT_URL environment variable. - (Authentication is based on environment variables, see - https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication. - The user/sp needs the key/encrypt and key/decrypt permissions.) - - To encrypt or decrypt using age, specify the recipient in the -a flag, - or in the SOPS_AGE_RECIPIENTS environment variable. - - To encrypt or decrypt using PGP, specify the PGP fingerprint in the - -p flag or in the SOPS_PGP_FP environment variable. - - To use multiple KMS or PGP keys, separate them by commas. For example: - $ sops -p "10F2...0A, 85D...B3F21" file.yaml - - The -p, -k, --gcp-kms, --hckms, --hc-vault-transit, and --azure-kv flags are only - used to encrypt new documents. Editing or decrypting existing documents - can be done with "sops file" or "sops decrypt file" respectively. The KMS and - PGP keys listed in the encrypted documents are used then. To manage master - keys in existing documents, use the "add-{kms,pgp,gcp-kms,hckms,azure-kv,hc-vault-transit}" - and "rm-{kms,pgp,gcp-kms,hckms,azure-kv,hc-vault-transit}" flags with --rotate - or the updatekeys command. - - To use a different GPG binary than the one in your PATH, set SOPS_GPG_EXEC. - - To select a different editor than the default (vim), set SOPS_EDITOR or - EDITOR. - - Note that flags must always be provided before the filename to operate on. - Otherwise, they will be ignored. - - For more information, see the README at https://github.com/getsops/sops` - app.EnableBashCompletion = true - app.Commands = []cli.Command{ - { - Name: "completion", - Usage: "Generate shell completion scripts", - Subcommands: []cli.Command{ - { - Name: "bash", - Usage: fmt.Sprintf("Generate bash completions. To load completions: `$ source <(%s completion bash)`", app.Name), - Action: func(c *cli.Context) error { - fmt.Fprint(c.App.Writer, GenBashCompletion(app.Name)) - return nil - }, - }, - { - Name: "zsh", - Usage: fmt.Sprintf("Generate zsh completions. To load completions: `$ source <(%s completion zsh)`", app.Name), - Action: func(c *cli.Context) error { - fmt.Fprint(c.App.Writer, GenZshCompletion(app.Name)) - return nil - }, - }}, - }, - { - Name: "exec-env", - Usage: "execute a command with decrypted values inserted into the environment", - ArgsUsage: "[file to decrypt] [command to run]", - Flags: append([]cli.Flag{ - cli.BoolFlag{ - Name: "background", - Usage: "background the process and don't wait for it to complete (DEPRECATED)", - }, - cli.BoolFlag{ - Name: "pristine", - Usage: "insert only the decrypted values into the environment without forwarding existing environment variables", - }, - cli.StringFlag{ - Name: "user", - Usage: "the user to run the command as", - }, - cli.BoolFlag{ - Name: "same-process", - Usage: "run command in the current process instead of in a child process", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", - }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.NArg() != 2 { - return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) - } + // Remove GLOBAL OPTIONS from the help text of subcommands + cli.CommandHelpTemplate = `NAME: + {{template "helpNameTemplate" .}} - fileName := c.Args()[0] - command := c.Args()[1] +USAGE: + {{template "usageTemplate" .}}{{if .Category}} - inputStore, err := inputStore(c, fileName) - if err != nil { - return toExitError(err) - } +CATEGORY: + {{.Category}}{{end}}{{if .Description}} - svcs := keyservices(c) +DESCRIPTION: + {{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}} - order, err := decryptionOrder(c.String("decryption-order")) - if err != nil { - return toExitError(err) - } - opts := decryptOpts{ - OutputStore: &dotenv.Store{}, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - KeyServices: svcs, - DecryptionOrder: order, - IgnoreMAC: c.Bool("ignore-mac"), - } +OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} - if c.Bool("background") { - log.Warn("exec-env's --background option is deprecated and will be removed in a future version of sops") +OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} +` - if c.Bool("same-process") { - return common.NewExitError("Error: The --same-process flag cannot be used with --background", codes.ErrorConflictingParameters) + keyserviceFlags := []cli.Flag{ + &cli.BoolFlag{ + Name: "enable-local-keyservice", + Value: true, + Usage: "use local key service", + Sources: cli.EnvVars("SOPS_ENABLE_LOCAL_KEYSERVICE"), + }, + &cli.StringSliceFlag{ + Name: "keyservice", + Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000", + Sources: cli.EnvVars("SOPS_KEYSERVICE"), + }, + } + + cmd := &cli.Command{ + Name: "sops", + Usage: "encrypted file editor with AWS KMS, GCP KMS, HuaweiCloud KMS, Azure Key Vault, age, and GPG support", + ArgsUsage: "sops [options] file", + Version: version.Version, + Authors: []any{"CNCF Maintainers"}, + + UsageText: `sops is an editor of encrypted files that supports AWS KMS, GCP, HuaweiCloud KMS, AZKV, +PGP, and Age + +To encrypt or decrypt a document with AWS KMS, specify the KMS ARN +in the -k flag or in the SOPS_KMS_ARN environment variable. +(you need valid credentials in ~/.aws/credentials or in your env) + +To encrypt or decrypt a document with GCP KMS, specify the +GCP KMS resource ID in the --gcp-kms flag or in the SOPS_GCP_KMS_IDS +environment variable. +(You need to setup Google application default credentials. See + https://developers.google.com/identity/protocols/application-default-credentials) + +To encrypt or decrypt a document with HuaweiCloud KMS, specify the +HuaweiCloud KMS key ID (format: region:key-uuid) in the --hckms flag or in the +SOPS_HUAWEICLOUD_KMS_IDS environment variable. +(You need to setup HuaweiCloud credentials via environment variables: + HUAWEICLOUD_SDK_AK, HUAWEICLOUD_SDK_SK, HUAWEICLOUD_SDK_PROJECT_ID, or + use credentials file at ~/.huaweicloud/credentials) + +To encrypt or decrypt a document with HashiCorp Vault's Transit Secret +Engine, specify the Vault key URI name in the --hc-vault-transit flag +or in the SOPS_VAULT_URIS environment variable (for example +https://vault.example.org:8200/v1/transit/keys/dev, where +'https://vault.example.org:8200' is the vault server, 'transit' the +enginePath, and 'dev' is the name of the key). +(You need to enable the Transit Secrets Engine in Vault. See + https://www.vaultproject.io/docs/secrets/transit/index.html) + +To encrypt or decrypt a document with Azure Key Vault, specify the +Azure Key Vault key URL in the --azure-kv flag or in the +SOPS_AZURE_KEYVAULT_URL environment variable. +(Authentication is based on environment variables, see + https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication. + The user/sp needs the key/encrypt and key/decrypt permissions.) + +To encrypt or decrypt using age, specify the recipient in the -a flag, +or in the SOPS_AGE_RECIPIENTS environment variable. + +To encrypt or decrypt using PGP, specify the PGP fingerprint in the +-p flag or in the SOPS_PGP_FP environment variable. + +To use multiple KMS or PGP keys, separate them by commas. For example: + $ sops -p "10F2...0A, 85D...B3F21" file.yaml + +The -p, -k, --gcp-kms, --hckms, --hc-vault-transit, and --azure-kv flags are only +used to encrypt new documents. Editing or decrypting existing documents +can be done with "sops file" or "sops decrypt file" respectively. The KMS and +PGP keys listed in the encrypted documents are used then. To manage master +keys in existing documents, use the "add-{kms,pgp,gcp-kms,hckms,azure-kv,hc-vault-transit}" +and "rm-{kms,pgp,gcp-kms,hckms,azure-kv,hc-vault-transit}" flags with --rotate +or the updatekeys command. + +To use a different GPG binary than the one in your PATH, set SOPS_GPG_EXEC. + +To select a different editor than the default (vim), set SOPS_EDITOR or +EDITOR. + +Note that flags must always be provided before the filename to operate on. +Otherwise, they will be ignored. + +For more information, see the README at https://github.com/getsops/sops`, + + EnableShellCompletion: true, + + Commands: []*cli.Command{ + { + Name: "exec-env", + Usage: "execute a command with decrypted values inserted into the environment", + ArgsUsage: "[file to decrypt] [command to run]", + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "background", + Usage: "background the process and don't wait for it to complete (DEPRECATED)", + }, + &cli.BoolFlag{ + Name: "pristine", + Usage: "insert only the decrypted values into the environment without forwarding existing environment variables", + }, + &cli.StringFlag{ + Name: "user", + Usage: "the user to run the command as", + }, + &cli.BoolFlag{ + Name: "same-process", + Usage: "run command in the current process instead of in a child process", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.NArg() != 2 { + return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) } - } - tree, err := decryptTree(opts) - if err != nil { - return toExitError(err) - } + fileName := c.Args().First() + command := c.Args().Get(1) - var env []string - for _, item := range tree.Branches[0] { - if stores.IsComplexValue(item.Value) { - return cli.NewExitError(fmt.Errorf("cannot use complex value in environment; offending key %s", item.Key), codes.ErrorGeneric) + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) } - if _, ok := item.Key.(sops.Comment); ok { - continue + + svcs := keyservices(c) + + order, err := decryptionOrder(c.String("decryption-order")) + if err != nil { + return toExitError(err) } - key, ok := item.Key.(string) - if !ok { - return cli.NewExitError(fmt.Errorf("cannot use non-string keys in environment, got %T", item.Key), codes.ErrorGeneric) + opts := decryptOpts{ + OutputStore: &dotenv.Store{}, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + KeyServices: svcs, + DecryptionOrder: order, + IgnoreMAC: c.Bool("ignore-mac"), } - if strings.Contains(key, "=") { - return cli.NewExitError(fmt.Errorf("cannot use keys with '=' in environment: %s", key), codes.ErrorGeneric) + + if c.Bool("background") { + log.Warn("exec-env's --background option is deprecated and will be removed in a future version of sops") + + if c.Bool("same-process") { + return common.NewExitError("Error: The --same-process flag cannot be used with --background", codes.ErrorConflictingParameters) + } } - value, ok := item.Value.(string) - if !ok { - value = stores.ValToString(item.Value) + + tree, err := decryptTree(opts) + if err != nil { + return toExitError(err) } - env = append(env, fmt.Sprintf("%s=%s", key, value)) - } - if err := exec.ExecWithEnv(exec.ExecOpts{ - Command: command, - Plaintext: []byte{}, - Background: c.Bool("background"), - Pristine: c.Bool("pristine"), - User: c.String("user"), - SameProcess: c.Bool("same-process"), - Env: env, - }); err != nil { - return toExitError(err) - } + var env []string + for _, item := range tree.Branches[0] { + if stores.IsComplexValue(item.Value) { + return cli.Exit(fmt.Errorf("cannot use complex value in environment; offending key %s", item.Key), codes.ErrorGeneric) + } + if _, ok := item.Key.(sops.Comment); ok { + continue + } + key, ok := item.Key.(string) + if !ok { + return cli.Exit(fmt.Errorf("cannot use non-string keys in environment, got %T", item.Key), codes.ErrorGeneric) + } + if strings.Contains(key, "=") { + return cli.Exit(fmt.Errorf("cannot use keys with '=' in environment: %s", key), codes.ErrorGeneric) + } + value, ok := item.Value.(string) + if !ok { + value = stores.ValToString(item.Value) + } + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } - return nil - }, - }, - { - Name: "exec-file", - Usage: "execute a command with the decrypted contents as a temporary file", - ArgsUsage: "[file to decrypt] [command to run]", - Flags: append([]cli.Flag{ - cli.BoolFlag{ - Name: "background", - Usage: "background the process and don't wait for it to complete (DEPRECATED)", - }, - cli.BoolFlag{ - Name: "no-fifo", - Usage: "use a regular file instead of a fifo to temporarily hold the decrypted contents", - }, - cli.StringFlag{ - Name: "user", - Usage: "the user to run the command as", - }, - cli.StringFlag{ - Name: "input-type", - Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.StringFlag{ - Name: "filename", - Usage: fmt.Sprintf("filename for the temporarily file (default: %s)", exec.FallbackFilename), - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", + if err := exec.ExecWithEnv(exec.ExecOpts{ + Command: command, + Plaintext: []byte{}, + Background: c.Bool("background"), + Pristine: c.Bool("pristine"), + User: c.String("user"), + SameProcess: c.Bool("same-process"), + Env: env, + }); err != nil { + return toExitError(err) + } + + return nil }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.NArg() != 2 { - return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) - } + }, + { + Name: "exec-file", + Usage: "execute a command with the decrypted contents as a temporary file", + ArgsUsage: "[file to decrypt] [command to run]", + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "background", + Usage: "background the process and don't wait for it to complete (DEPRECATED)", + }, + &cli.BoolFlag{ + Name: "no-fifo", + Usage: "use a regular file instead of a fifo to temporarily hold the decrypted contents", + }, + &cli.StringFlag{ + Name: "user", + Usage: "the user to run the command as", + }, + &cli.StringFlag{ + Name: "input-type", + Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.StringFlag{ + Name: "filename", + Usage: fmt.Sprintf("filename for the temporarily file (default: %s)", exec.FallbackFilename), + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.NArg() != 2 { + return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) + } - fileName := c.Args()[0] - command := c.Args()[1] + fileName := c.Args().First() + command := c.Args().Get(1) - inputStore, err := inputStore(c, fileName) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, fileName) - if err != nil { - return toExitError(err) - } + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) + } - svcs := keyservices(c) + svcs := keyservices(c) - order, err := decryptionOrder(c.String("decryption-order")) - if err != nil { - return toExitError(err) - } - opts := decryptOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - KeyServices: svcs, - DecryptionOrder: order, - IgnoreMAC: c.Bool("ignore-mac"), - } + order, err := decryptionOrder(c.String("decryption-order")) + if err != nil { + return toExitError(err) + } + opts := decryptOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + KeyServices: svcs, + DecryptionOrder: order, + IgnoreMAC: c.Bool("ignore-mac"), + } - output, err := decrypt(opts) - if err != nil { - return toExitError(err) - } + output, err := decrypt(opts) + if err != nil { + return toExitError(err) + } - if c.Bool("background") { - log.Warn("exec-file's --background option is deprecated and will be removed in a future version of sops") - } + if c.Bool("background") { + log.Warn("exec-file's --background option is deprecated and will be removed in a future version of sops") + } - if err := exec.ExecWithFile(exec.ExecOpts{ - Command: command, - Plaintext: output, - Background: c.Bool("background"), - Fifo: !c.Bool("no-fifo"), - User: c.String("user"), - Filename: c.String("filename"), - }); err != nil { - return toExitError(err) - } + if err := exec.ExecWithFile(exec.ExecOpts{ + Command: command, + Plaintext: output, + Background: c.Bool("background"), + Fifo: !c.Bool("no-fifo"), + User: c.String("user"), + Filename: c.String("filename"), + }); err != nil { + return toExitError(err) + } - return nil - }, - }, - { - Name: "publish", - Usage: "Publish sops file or directory to a configured destination", - ArgsUsage: `file`, - Flags: append([]cli.Flag{ - cli.BoolFlag{ - Name: "yes, y", - Usage: `pre-approve all changes and run non-interactively`, - }, - cli.BoolFlag{ - Name: "omit-extensions", - Usage: "Omit file extensions in destination path when publishing sops file to configured destinations", - }, - cli.BoolFlag{ - Name: "recursive", - Usage: "If the source path is a directory, publish all its content recursively", - }, - cli.BoolFlag{ - Name: "verbose", - Usage: "Enable verbose logging output", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", + return nil }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.Bool("verbose") || c.GlobalBool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - var configPath string - var err error - if c.GlobalString("config") != "" { - configPath = c.GlobalString("config") - } else { - configPath, err = findConfigFile() + }, + { + Name: "publish", + Usage: "Publish sops file or directory to a configured destination", + ArgsUsage: `file`, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "yes, y", + Usage: `pre-approve all changes and run non-interactively`, + }, + &cli.BoolFlag{ + Name: "omit-extensions", + Usage: "Omit file extensions in destination path when publishing sops file to configured destinations", + }, + &cli.BoolFlag{ + Name: "recursive", + Usage: "If the source path is a directory, publish all its content recursively", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Enable verbose logging output", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) + } + var configPath string + var err error + if c.String("config") != "" { + configPath = c.String("config") + } else { + configPath, err = findConfigFile() + if err != nil { + return common.NewExitError(err, codes.ErrorGeneric) + } + } + if c.NArg() < 1 { + return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + } + warnMoreThanOnePositionalArgument(c) + path := c.Args().First() + info, err := os.Stat(path) if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + return toExitError(err) } - } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) - } - warnMoreThanOnePositionalArgument(c) - path := c.Args()[0] - info, err := os.Stat(path) - if err != nil { - return toExitError(err) - } - if info.IsDir() && !c.Bool("recursive") { - return fmt.Errorf("can't operate on a directory without --recursive flag.") - } - order, err := decryptionOrder(c.String("decryption-order")) - if err != nil { - return toExitError(err) - } - err = filepath.Walk(path, func(subPath string, info os.FileInfo, err error) error { + if info.IsDir() && !c.Bool("recursive") { + return fmt.Errorf("can't operate on a directory without --recursive flag.") + } + order, err := decryptionOrder(c.String("decryption-order")) if err != nil { return toExitError(err) } - if !info.IsDir() { - inputStore, err := inputStore(c, subPath) + err = filepath.Walk(path, func(subPath string, info os.FileInfo, err error) error { if err != nil { return toExitError(err) } - err = publishcmd.Run(publishcmd.Opts{ - ConfigPath: configPath, - InputPath: subPath, - RootPath: path, - Cipher: aes.NewCipher(), - KeyServices: keyservices(c), - DecryptionOrder: order, - InputStore: inputStore, - Interactive: !c.Bool("yes"), - OmitExtensions: c.Bool("omit-extensions"), - Recursive: c.Bool("recursive"), - }) - if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil { - return cliErr - } else if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + if !info.IsDir() { + inputStore, err := inputStore(c, subPath) + if err != nil { + return toExitError(err) + } + err = publishcmd.Run(publishcmd.Opts{ + ConfigPath: configPath, + InputPath: subPath, + RootPath: path, + Cipher: aes.NewCipher(), + KeyServices: keyservices(c), + DecryptionOrder: order, + InputStore: inputStore, + Interactive: !c.Bool("yes"), + OmitExtensions: c.Bool("omit-extensions"), + Recursive: c.Bool("recursive"), + }) + if cliErr, ok := err.(cli.ExitCoder); ok && cliErr != nil { + return cliErr + } else if err != nil { + return common.NewExitError(err, codes.ErrorGeneric) + } } + return nil + }) + if err != nil { + return toExitError(err) } return nil - }) - if err != nil { - return toExitError(err) - } - return nil - }, - }, - { - Name: "keyservice", - Usage: "start a SOPS key service server", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "network, net", - Usage: "network to listen on, e.g. 'tcp' or 'unix'", - Value: "tcp", }, - cli.StringFlag{ - Name: "address, addr", - Usage: "address to listen on, e.g. '127.0.0.1:5000' or '/tmp/sops.sock'", - Value: "127.0.0.1:5000", - }, - cli.BoolFlag{ - Name: "prompt", - Usage: "Prompt user to confirm every incoming request", + }, + { + Name: "keyservice", + Usage: "start a SOPS key service server", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "network, net", + Usage: "network to listen on, e.g. 'tcp' or 'unix'", + Value: "tcp", + }, + &cli.StringFlag{ + Name: "address, addr", + Usage: "address to listen on, e.g. '127.0.0.1:5000' or '/tmp/sops.sock'", + Value: "127.0.0.1:5000", + }, + &cli.BoolFlag{ + Name: "prompt", + Usage: "Prompt user to confirm every incoming request", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Enable verbose logging output", + }, }, - cli.BoolFlag{ - Name: "verbose", - Usage: "Enable verbose logging output", + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) + } + err := keyservicecmd.Run(keyservicecmd.Opts{ + Network: c.String("network"), + Address: c.String("address"), + Prompt: c.Bool("prompt"), + }) + if err != nil { + log.Errorf("Error running keyservice: %s", err) + return err + } + return nil }, }, - Action: func(c *cli.Context) error { - if c.Bool("verbose") || c.GlobalBool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - err := keyservicecmd.Run(keyservicecmd.Opts{ - Network: c.String("network"), - Address: c.String("address"), - Prompt: c.Bool("prompt"), - }) - if err != nil { - log.Errorf("Error running keyservice: %s", err) - return err - } - return nil - }, - }, - { - Name: "filestatus", - Usage: "check the status of the file, returning encryption status", - ArgsUsage: `file`, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "input-type", - Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + { + Name: "filestatus", + Usage: "check the status of the file, returning encryption status", + ArgsUsage: `file`, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "input-type", + Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, }, - }, - Action: func(c *cli.Context) error { - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) - } + Action: func(ctx context.Context, c *cli.Command) error { + if c.NArg() < 1 { + return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + } - fileName := c.Args()[0] - inputStore, err := inputStore(c, fileName) - if err != nil { - return toExitError(err) - } - opts := filestatuscmd.Opts{ - InputStore: inputStore, - InputPath: fileName, - } + fileName := c.Args().First() + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + opts := filestatuscmd.Opts{ + InputStore: inputStore, + InputPath: fileName, + } - status, err := filestatuscmd.FileStatus(opts) - if err != nil { - return err - } + status, err := filestatuscmd.FileStatus(opts) + if err != nil { + return err + } - json, err := encodingjson.Marshal(status) - if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) - } + json, err := encodingjson.Marshal(status) + if err != nil { + return common.NewExitError(err, codes.ErrorGeneric) + } - fmt.Println(string(json)) + fmt.Println(string(json)) - return nil + return nil + }, }, - }, - { - Name: "groups", - Usage: "modify the groups on a SOPS file", - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "add a new group to a SOPS file", - Flags: append([]cli.Flag{ - cli.StringFlag{ - Name: "file, f", - Usage: "the file to add the group to", - }, - cli.StringSliceFlag{ - Name: "pgp", - Usage: "the PGP fingerprints the new group should contain. Can be specified more than once", - }, - cli.StringSliceFlag{ - Name: "kms", - Usage: "the KMS ARNs the new group should contain. Can be specified more than once", - }, - cli.StringFlag{ - Name: "aws-profile", - Usage: "The AWS profile to use for requests to AWS", - }, - cli.StringSliceFlag{ - Name: "gcp-kms", - Usage: "the GCP KMS Resource ID the new group should contain. Can be specified more than once", - }, - cli.StringSliceFlag{ - Name: "hckms", - Usage: "the HuaweiCloud KMS key ID (format: region:key-uuid) the new group should contain. Can be specified more than once", - }, - cli.StringSliceFlag{ - Name: "azure-kv", - Usage: "the Azure Key Vault key URL the new group should contain. Can be specified more than once", - }, - cli.StringSliceFlag{ - Name: "hc-vault-transit", - Usage: "the full vault path to the key used to encrypt/decrypt. Make you choose and configure a key with encryption/decryption enabled (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev'). Can be specified more than once", - }, - cli.StringSliceFlag{ - Name: "age", - Usage: "the age recipient the new group should contain. Can be specified more than once", - }, - cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", - }, - cli.IntFlag{ - Name: "shamir-secret-sharing-threshold", - Usage: "the number of master keys required to retrieve the data key with shamir", - }, - cli.StringFlag{ - Name: "encryption-context", - Usage: "comma separated list of KMS encryption context key:value pairs", - }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - pgpFps := c.StringSlice("pgp") - kmsArns := c.StringSlice("kms") - gcpKmses := c.StringSlice("gcp-kms") - vaultURIs := c.StringSlice("hc-vault-transit") - azkvs := c.StringSlice("azure-kv") - ageRecipients := c.StringSlice("age") - if c.NArg() != 0 { - return common.NewExitError(fmt.Errorf("error: no positional arguments allowed"), codes.ErrorGeneric) - } - var group sops.KeyGroup - for _, fp := range pgpFps { - group = append(group, pgp.NewMasterKeyFromFingerprint(fp)) - } - for _, arn := range kmsArns { - group = append(group, kms.NewMasterKeyFromArn(arn, kms.ParseKMSContext(c.String("encryption-context")), c.String("aws-profile"))) - } - for _, kms := range gcpKmses { - group = append(group, gcpkms.NewMasterKeyFromResourceID(kms)) - } - for _, uri := range vaultURIs { - k, err := hcvault.NewMasterKeyFromURI(uri) + { + Name: "groups", + Usage: "modify the groups on a SOPS file", + Commands: []*cli.Command{ + { + Name: "add", + Usage: "add a new group to a SOPS file", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "file, f", + Usage: "the file to add the group to", + }, + &cli.StringSliceFlag{ + Name: "pgp", + Usage: "the PGP fingerprints the new group should contain. Can be specified more than once", + }, + &cli.StringSliceFlag{ + Name: "kms", + Usage: "the KMS ARNs the new group should contain. Can be specified more than once", + }, + &cli.StringFlag{ + Name: "aws-profile", + Usage: "The AWS profile to use for requests to AWS", + }, + &cli.StringSliceFlag{ + Name: "gcp-kms", + Usage: "the GCP KMS Resource ID the new group should contain. Can be specified more than once", + }, + &cli.StringSliceFlag{ + Name: "hckms", + Usage: "the HuaweiCloud KMS key ID (format: region:key-uuid) the new group should contain. Can be specified more than once", + }, + &cli.StringSliceFlag{ + Name: "azure-kv", + Usage: "the Azure Key Vault key URL the new group should contain. Can be specified more than once", + }, + &cli.StringSliceFlag{ + Name: "hc-vault-transit", + Usage: "the full vault path to the key used to encrypt/decrypt. Make you choose and configure a key with encryption/decryption enabled (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev'). Can be specified more than once", + }, + &cli.StringSliceFlag{ + Name: "age", + Usage: "the age recipient the new group should contain. Can be specified more than once", + }, + &cli.BoolFlag{ + Name: "in-place, i", + Usage: "write output back to the same file instead of stdout", + }, + &cli.IntFlag{ + Name: "shamir-secret-sharing-threshold", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, + &cli.StringFlag{ + Name: "encryption-context", + Usage: "comma separated list of KMS encryption context key:value pairs", + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + pgpFps := c.StringSlice("pgp") + kmsArns := c.StringSlice("kms") + gcpKmses := c.StringSlice("gcp-kms") + vaultURIs := c.StringSlice("hc-vault-transit") + azkvs := c.StringSlice("azure-kv") + ageRecipients := c.StringSlice("age") + if c.NArg() != 0 { + return common.NewExitError(fmt.Errorf("error: no positional arguments allowed"), codes.ErrorGeneric) + } + var group sops.KeyGroup + for _, fp := range pgpFps { + group = append(group, pgp.NewMasterKeyFromFingerprint(fp)) + } + for _, arn := range kmsArns { + group = append(group, kms.NewMasterKeyFromArn(arn, kms.ParseKMSContext(c.String("encryption-context")), c.String("aws-profile"))) + } + for _, kms := range gcpKmses { + group = append(group, gcpkms.NewMasterKeyFromResourceID(kms)) + } + for _, uri := range vaultURIs { + k, err := hcvault.NewMasterKeyFromURI(uri) + if err != nil { + log.WithError(err).Error("Failed to add key") + continue + } + group = append(group, k) + } + for _, url := range azkvs { + k, err := azkv.NewMasterKeyFromURL(url) + if err != nil { + log.WithError(err).Error("Failed to add key") + continue + } + group = append(group, k) + } + for _, recipient := range ageRecipients { + keys, err := age.MasterKeysFromRecipients(recipient) + if err != nil { + log.WithError(err).Error("Failed to add key") + continue + } + for _, key := range keys { + group = append(group, key) + } + } + inputStore, err := inputStore(c, c.String("file")) if err != nil { - log.WithError(err).Error("Failed to add key") - continue + return toExitError(err) } - group = append(group, k) - } - for _, url := range azkvs { - k, err := azkv.NewMasterKeyFromURL(url) + outputStore, err := outputStore(c, c.String("file")) if err != nil { - log.WithError(err).Error("Failed to add key") - continue + return toExitError(err) } - group = append(group, k) - } - for _, recipient := range ageRecipients { - keys, err := age.MasterKeysFromRecipients(recipient) + return groups.Add(groups.AddOpts{ + InputPath: c.String("file"), + InPlace: c.Bool("in-place"), + InputStore: inputStore, + OutputStore: outputStore, + Group: group, + GroupThreshold: c.Int("shamir-secret-sharing-threshold"), + KeyServices: keyservices(c), + }) + }, + }, + { + Name: "delete", + Usage: "delete a key group from a SOPS file", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "file, f", + Usage: "the file to add the group to", + }, + &cli.BoolFlag{ + Name: "in-place, i", + Usage: "write output back to the same file instead of stdout", + }, + &cli.IntFlag{ + Name: "shamir-secret-sharing-threshold", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, + }, keyserviceFlags...), + ArgsUsage: `[index]`, + + Action: func(ctx context.Context, c *cli.Command) error { + if c.NArg() != 1 { + return common.NewExitError(fmt.Errorf("error: exactly one positional argument (index) required"), codes.ErrorGeneric) + } + group, err := strconv.ParseUint(c.Args().First(), 10, 32) if err != nil { - log.WithError(err).Error("Failed to add key") - continue + return fmt.Errorf("failed to parse [index] argument: %s", err) } - for _, key := range keys { - group = append(group, key) + + inputStore, err := inputStore(c, c.String("file")) + if err != nil { + return toExitError(err) } - } - inputStore, err := inputStore(c, c.String("file")) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, c.String("file")) - if err != nil { - return toExitError(err) - } - return groups.Add(groups.AddOpts{ - InputPath: c.String("file"), - InPlace: c.Bool("in-place"), - InputStore: inputStore, - OutputStore: outputStore, - Group: group, - GroupThreshold: c.Int("shamir-secret-sharing-threshold"), - KeyServices: keyservices(c), - }) + outputStore, err := outputStore(c, c.String("file")) + if err != nil { + return toExitError(err) + } + return groups.Delete(groups.DeleteOpts{ + InputPath: c.String("file"), + InPlace: c.Bool("in-place"), + InputStore: inputStore, + OutputStore: outputStore, + Group: uint(group), + GroupThreshold: c.Int("shamir-secret-sharing-threshold"), + KeyServices: keyservices(c), + }) + }, }, }, - { - Name: "delete", - Usage: "delete a key group from a SOPS file", - Flags: append([]cli.Flag{ - cli.StringFlag{ - Name: "file, f", - Usage: "the file to add the group to", - }, - cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", - }, - cli.IntFlag{ - Name: "shamir-secret-sharing-threshold", - Usage: "the number of master keys required to retrieve the data key with shamir", - }, - }, keyserviceFlags...), - ArgsUsage: `[index]`, - - Action: func(c *cli.Context) error { - if c.NArg() != 1 { - return common.NewExitError(fmt.Errorf("error: exactly one positional argument (index) required"), codes.ErrorGeneric) - } - group, err := strconv.ParseUint(c.Args().First(), 10, 32) + }, + { + Name: "updatekeys", + Usage: "update the keys of SOPS files using the config file", + ArgsUsage: `file`, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "yes, y", + Usage: `pre-approve all changes and run non-interactively`, + }, + &cli.StringFlag{ + Name: "input-type", + Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + var err error + var configPath string + if c.String("config") != "" { + configPath = c.String("config") + } else { + configPath, err = findConfigFile() if err != nil { - return fmt.Errorf("failed to parse [index] argument: %s", err) + return common.NewExitError(err, codes.ErrorGeneric) } + } + if c.NArg() < 1 { + return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + } + failedCounter := 0 + for _, path := range c.Args().Slice() { + err := updatekeys.UpdateKeys(updatekeys.Opts{ + InputPath: path, + ShamirThreshold: c.Int("shamir-secret-sharing-threshold"), + KeyServices: keyservices(c), + Interactive: !c.Bool("yes"), + ConfigPath: configPath, + InputType: c.String("input-type"), + }) - inputStore, err := inputStore(c, c.String("file")) - if err != nil { - return toExitError(err) + if c.NArg() == 1 { + // a single argument was given, keep compatibility of the error + if cliErr, ok := err.(cli.ExitCoder); ok && cliErr != nil { + return cliErr + } else if err != nil { + return common.NewExitError(err, codes.ErrorGeneric) + } } - outputStore, err := outputStore(c, c.String("file")) + + // multiple arguments given (patched functionality), + // finish updating of remaining files and fail afterwards if err != nil { - return toExitError(err) + failedCounter++ + log.Error(err) } - return groups.Delete(groups.DeleteOpts{ - InputPath: c.String("file"), - InPlace: c.Bool("in-place"), - InputStore: inputStore, - OutputStore: outputStore, - Group: uint(group), - GroupThreshold: c.Int("shamir-secret-sharing-threshold"), - KeyServices: keyservices(c), - }) - }, + } + if failedCounter > 0 { + return common.NewExitError(fmt.Errorf("failed updating %d key(s)", failedCounter), codes.ErrorGeneric) + } + return nil }, }, - }, - { - Name: "updatekeys", - Usage: "update the keys of SOPS files using the config file", - ArgsUsage: `file`, - Flags: append([]cli.Flag{ - cli.BoolFlag{ - Name: "yes, y", - Usage: `pre-approve all changes and run non-interactively`, - }, - cli.StringFlag{ - Name: "input-type", - Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - var err error - var configPath string - if c.GlobalString("config") != "" { - configPath = c.GlobalString("config") - } else { - configPath, err = findConfigFile() - if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + { + Name: "decrypt", + Usage: "decrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", + ArgsUsage: `[file]`, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "in-place, i", + Usage: "write output back to the same file instead of stdout", + }, + &cli.StringFlag{ + Name: "extract", + Usage: "extract a specific key or branch from the input document. Example: --extract '[\"somekey\"][0]'", + }, + &cli.StringFlag{ + Name: "output", + Usage: "Save the output after decryption to the file specified", + }, + &cli.StringFlag{ + Name: "input-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.BoolFlag{ + Name: "ignore-mac", + Usage: "ignore Message Authentication Code during decryption", + }, + &cli.StringFlag{ + Name: "filename-override", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Should be provided when reading from stdin.", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) } - } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) - } - failedCounter := 0 - for _, path := range c.Args() { - err := updatekeys.UpdateKeys(updatekeys.Opts{ - InputPath: path, - ShamirThreshold: c.Int("shamir-secret-sharing-threshold"), - KeyServices: keyservices(c), - Interactive: !c.Bool("yes"), - ConfigPath: configPath, - InputType: c.String("input-type"), - }) - - if c.NArg() == 1 { - // a single argument was given, keep compatibility of the error - if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil { - return cliErr - } else if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + readFromStdin := c.NArg() == 0 + if readFromStdin && c.Bool("in-place") { + return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) + } + warnMoreThanOnePositionalArgument(c) + if c.Bool("in-place") && c.String("output") != "" { + return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) + } + var fileName string + var err error + if !readFromStdin { + fileName, err = filepath.Abs(c.Args().First()) + if err != nil { + return toExitError(err) + } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + } + } + fileNameOverride := c.String("filename-override") + if fileNameOverride == "" { + fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) } } - // multiple arguments given (patched functionality), - // finish updating of remaining files and fail afterwards + inputStore, err := inputStore(c, fileNameOverride) if err != nil { - failedCounter++ - log.Error(err) + return toExitError(err) } - } - if failedCounter > 0 { - return common.NewExitError(fmt.Errorf("failed updating %d key(s)", failedCounter), codes.ErrorGeneric) - } - return nil - }, - }, - { - Name: "decrypt", - Usage: "decrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", - ArgsUsage: `[file]`, - Flags: append([]cli.Flag{ - cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", - }, - cli.StringFlag{ - Name: "extract", - Usage: "extract a specific key or branch from the input document. Example: --extract '[\"somekey\"][0]'", - }, - cli.StringFlag{ - Name: "output", - Usage: "Save the output after decryption to the file specified", - }, - cli.StringFlag{ - Name: "input-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.BoolFlag{ - Name: "ignore-mac", - Usage: "ignore Message Authentication Code during decryption", - }, - cli.StringFlag{ - Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Should be provided when reading from stdin.", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", - }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.Bool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - readFromStdin := c.NArg() == 0 - if readFromStdin && c.Bool("in-place") { - return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) - } - warnMoreThanOnePositionalArgument(c) - if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) - } - var fileName string - var err error - if !readFromStdin { - fileName, err = filepath.Abs(c.Args()[0]) + outputStore, err := outputStore(c, fileNameOverride) if err != nil { return toExitError(err) } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) - } - } - fileNameOverride := c.String("filename-override") - if fileNameOverride == "" { - fileNameOverride = fileName - } else { - fileNameOverride, err = filepath.Abs(fileNameOverride) + svcs := keyservices(c) + + order, err := decryptionOrder(c.String("decryption-order")) if err != nil { return toExitError(err) } - } - - inputStore, err := inputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - svcs := keyservices(c) - - order, err := decryptionOrder(c.String("decryption-order")) - if err != nil { - return toExitError(err) - } - - var extract []interface{} - extract, err = parseTreePath(c.String("extract")) - if err != nil { - return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) - } - output, err := decrypt(decryptOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - ReadFromStdin: readFromStdin, - Cipher: aes.NewCipher(), - Extract: extract, - KeyServices: svcs, - DecryptionOrder: order, - IgnoreMAC: c.Bool("ignore-mac"), - }) - if err != nil { - return toExitError(err) - } - // We open the file *after* the operations on the tree have been - // executed to avoid truncating it when there's errors - if c.Bool("in-place") { - file, err := os.Create(fileName) + var extract []interface{} + extract, err = parseTreePath(c.String("extract")) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) } - defer file.Close() - _, err = file.Write(output) + output, err := decrypt(decryptOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + ReadFromStdin: readFromStdin, + Cipher: aes.NewCipher(), + Extract: extract, + KeyServices: svcs, + DecryptionOrder: order, + IgnoreMAC: c.Bool("ignore-mac"), + }) if err != nil { return toExitError(err) } - log.Info("File written successfully") - return nil - } - outputFile := os.Stdout - if c.String("output") != "" { - file, err := os.Create(c.String("output")) - if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + // We open the file *after* the operations on the tree have been + // executed to avoid truncating it when there's errors + if c.Bool("in-place") { + file, err := os.Create(fileName) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + _, err = file.Write(output) + if err != nil { + return toExitError(err) + } + log.Info("File written successfully") + return nil } - defer file.Close() - outputFile = file - } - _, err = outputFile.Write(output) - return toExitError(err) - }, - }, - { - Name: "encrypt", - Usage: "encrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", - ArgsUsage: `[file]`, - Flags: append([]cli.Flag{ - cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", - }, - cli.StringFlag{ - Name: "output", - Usage: "Save the output after encryption to the file specified", - }, - cli.StringFlag{ - Name: "kms, k", - Usage: "comma separated list of KMS ARNs", - EnvVar: "SOPS_KMS_ARN", - }, - cli.StringFlag{ - Name: "aws-profile", - Usage: "The AWS profile to use for requests to AWS", - }, - cli.StringFlag{ - Name: "gcp-kms", - Usage: "comma separated list of GCP KMS resource IDs", - EnvVar: "SOPS_GCP_KMS_IDS", - }, - cli.StringFlag{ - Name: "hckms", - Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", - EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS", - }, - cli.StringFlag{ - Name: "azure-kv", - Usage: "comma separated list of Azure Key Vault URLs", - EnvVar: "SOPS_AZURE_KEYVAULT_URLS", - }, - cli.StringFlag{ - Name: "hc-vault-transit", - Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')", - EnvVar: "SOPS_VAULT_URIS", - }, - cli.StringFlag{ - Name: "pgp, p", - Usage: "comma separated list of PGP fingerprints", - EnvVar: "SOPS_PGP_FP", - }, - cli.StringFlag{ - Name: "age, a", - Usage: "comma separated list of age recipients", - EnvVar: "SOPS_AGE_RECIPIENTS", - }, - cli.StringFlag{ - Name: "input-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.StringFlag{ - Name: "unencrypted-suffix", - Usage: "override the unencrypted key suffix.", - }, - cli.StringFlag{ - Name: "encrypted-suffix", - Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.", - }, - cli.StringFlag{ - Name: "unencrypted-regex", - Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", - }, - cli.StringFlag{ - Name: "encrypted-regex", - Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", - }, - cli.StringFlag{ - Name: "encryption-context", - Usage: "comma separated list of KMS encryption context key:value pairs", - }, - cli.IntFlag{ - Name: "shamir-secret-sharing-threshold", - Usage: "the number of master keys required to retrieve the data key with shamir", - }, - cli.StringFlag{ - Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Required when reading from stdin.", + + outputFile := os.Stdout + if c.String("output") != "" { + file, err := os.Create(c.String("output")) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + outputFile = file + } + _, err = outputFile.Write(output) + return toExitError(err) }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.Bool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - readFromStdin := c.NArg() == 0 - if readFromStdin { - if c.Bool("in-place") { - return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) + }, + { + Name: "encrypt", + Usage: "encrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", + ArgsUsage: `[file]`, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "in-place, i", + Usage: "write output back to the same file instead of stdout", + }, + &cli.StringFlag{ + Name: "output", + Usage: "Save the output after encryption to the file specified", + }, + &cli.StringFlag{ + Name: "kms, k", + Usage: "comma separated list of KMS ARNs", + Sources: cli.EnvVars("SOPS_KMS_ARN"), + }, + &cli.StringFlag{ + Name: "aws-profile", + Usage: "The AWS profile to use for requests to AWS", + }, + &cli.StringFlag{ + Name: "gcp-kms", + Usage: "comma separated list of GCP KMS resource IDs", + Sources: cli.EnvVars("SOPS_GCP_KMS_IDS"), + }, + &cli.StringFlag{ + Name: "hckms", + Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", + Sources: cli.EnvVars("SOPS_HUAWEICLOUD_KMS_IDS"), + }, + &cli.StringFlag{ + Name: "azure-kv", + Usage: "comma separated list of Azure Key Vault URLs", + Sources: cli.EnvVars("SOPS_AZURE_KEYVAULT_URLS"), + }, + &cli.StringFlag{ + Name: "hc-vault-transit", + Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')", + Sources: cli.EnvVars("SOPS_VAULT_URIS"), + }, + &cli.StringFlag{ + Name: "pgp, p", + Usage: "comma separated list of PGP fingerprints", + Sources: cli.EnvVars("SOPS_PGP_FP"), + }, + &cli.StringFlag{ + Name: "age, a", + Usage: "comma separated list of age recipients", + Sources: cli.EnvVars("SOPS_AGE_RECIPIENTS"), + }, + &cli.StringFlag{ + Name: "input-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.StringFlag{ + Name: "unencrypted-suffix", + Usage: "override the unencrypted key suffix.", + }, + &cli.StringFlag{ + Name: "encrypted-suffix", + Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.", + }, + &cli.StringFlag{ + Name: "unencrypted-regex", + Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", + }, + &cli.StringFlag{ + Name: "encrypted-regex", + Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", + }, + &cli.StringFlag{ + Name: "encryption-context", + Usage: "comma separated list of KMS encryption context key:value pairs", + }, + &cli.IntFlag{ + Name: "shamir-secret-sharing-threshold", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, + &cli.StringFlag{ + Name: "filename-override", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Required when reading from stdin.", + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) + } + readFromStdin := c.NArg() == 0 + if readFromStdin { + if c.Bool("in-place") { + return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) + } + if c.String("filename-override") == "" { + return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters) + } } - if c.String("filename-override") == "" { - return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters) + warnMoreThanOnePositionalArgument(c) + if c.Bool("in-place") && c.String("output") != "" { + return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } - } - warnMoreThanOnePositionalArgument(c) - if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) - } - var fileName string - var err error - if !readFromStdin { - fileName, err = filepath.Abs(c.Args()[0]) + var fileName string + var err error + if !readFromStdin { + fileName, err = filepath.Abs(c.Args().First()) + if err != nil { + return toExitError(err) + } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + } + } + fileNameOverride := c.String("filename-override") + if fileNameOverride == "" { + fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } + } + + inputStore, err := inputStore(c, fileNameOverride) if err != nil { return toExitError(err) } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) - } - } - fileNameOverride := c.String("filename-override") - if fileNameOverride == "" { - fileNameOverride = fileName - } else { - fileNameOverride, err = filepath.Abs(fileNameOverride) + outputStore, err := outputStore(c, fileNameOverride) if err != nil { return toExitError(err) } - } - - inputStore, err := inputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - svcs := keyservices(c) - - encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, nil) - if err != nil { - return toExitError(err) - } - output, err := encrypt(encryptOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - ReadFromStdin: readFromStdin, - Cipher: aes.NewCipher(), - KeyServices: svcs, - encryptConfig: encConfig, - }) - - if err != nil { - return toExitError(err) - } + svcs := keyservices(c) - // We open the file *after* the operations on the tree have been - // executed to avoid truncating it when there's errors - if c.Bool("in-place") { - file, err := os.Create(fileName) + encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, nil) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return toExitError(err) } - defer file.Close() - _, err = file.Write(output) + output, err := encrypt(encryptOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + ReadFromStdin: readFromStdin, + Cipher: aes.NewCipher(), + KeyServices: svcs, + encryptConfig: encConfig, + }) if err != nil { return toExitError(err) } - log.Info("File written successfully") - return nil - } - outputFile := os.Stdout - if c.String("output") != "" { - file, err := os.Create(c.String("output")) - if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + // We open the file *after* the operations on the tree have been + // executed to avoid truncating it when there's errors + if c.Bool("in-place") { + file, err := os.Create(fileName) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + _, err = file.Write(output) + if err != nil { + return toExitError(err) + } + log.Info("File written successfully") + return nil } - defer file.Close() - outputFile = file - } - _, err = outputFile.Write(output) - return toExitError(err) - }, - }, - { - Name: "rotate", - Usage: "generate a new data encryption key and reencrypt all values with the new key", - ArgsUsage: `file`, - Flags: append([]cli.Flag{ - cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", - }, - cli.StringFlag{ - Name: "output", - Usage: "Save the output after decryption to the file specified", - }, - cli.StringFlag{ - Name: "input-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.StringFlag{ - Name: "encryption-context", - Usage: "comma separated list of KMS encryption context key:value pairs", - }, - cli.StringFlag{ - Name: "add-gcp-kms", - Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-gcp-kms", - Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-hckms", - Usage: "add the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) to the list of master keys on the given file", + + outputFile := os.Stdout + if c.String("output") != "" { + file, err := os.Create(c.String("output")) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + outputFile = file + } + _, err = outputFile.Write(output) + return toExitError(err) }, - cli.StringFlag{ - Name: "rm-hckms", - Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-azure-kv", - Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-azure-kv", - Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-kms", - Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-kms", - Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-hc-vault-transit", - Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", - }, - cli.StringFlag{ - Name: "rm-hc-vault-transit", - Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", - }, - cli.StringFlag{ - Name: "add-age", - Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-age", - Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-pgp", - Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-pgp", - Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", - }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.Bool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) - } - warnMoreThanOnePositionalArgument(c) - if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) - } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || - c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { - return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) + }, + { + Name: "rotate", + Usage: "generate a new data encryption key and reencrypt all values with the new key", + ArgsUsage: `file`, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "in-place, i", + Usage: "write output back to the same file instead of stdout", + }, + &cli.StringFlag{ + Name: "output", + Usage: "Save the output after decryption to the file specified", + }, + &cli.StringFlag{ + Name: "input-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.StringFlag{ + Name: "encryption-context", + Usage: "comma separated list of KMS encryption context key:value pairs", + }, + &cli.StringFlag{ + Name: "add-gcp-kms", + Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-gcp-kms", + Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-hckms", + Usage: "add the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-hckms", + Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-azure-kv", + Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-azure-kv", + Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-kms", + Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-kms", + Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-hc-vault-transit", + Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", + }, + &cli.StringFlag{ + Name: "rm-hc-vault-transit", + Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", + }, + &cli.StringFlag{ + Name: "add-age", + Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-age", + Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-pgp", + Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-pgp", + Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "filename-override", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) } - } - fileNameOverride := c.String("filename-override") - if fileNameOverride == "" { - fileNameOverride = fileName - } else { - fileNameOverride, err = filepath.Abs(fileNameOverride) + if c.NArg() < 1 { + return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + } + warnMoreThanOnePositionalArgument(c) + if c.Bool("in-place") && c.String("output") != "" { + return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) + } + fileName, err := filepath.Abs(c.Args().First()) if err != nil { return toExitError(err) } - } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || + c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { + return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) + } + } + fileNameOverride := c.String("filename-override") + if fileNameOverride == "" { + fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } + } - inputStore, err := inputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - svcs := keyservices(c) + inputStore, err := inputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + svcs := keyservices(c) - order, err := decryptionOrder(c.String("decryption-order")) - if err != nil { - return toExitError(err) - } + order, err := decryptionOrder(c.String("decryption-order")) + if err != nil { + return toExitError(err) + } - rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order) - if err != nil { - return toExitError(err) - } - output, err := rotate(rotateOpts) - if err != nil { + rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order) + if err != nil { + return toExitError(err) + } + output, err := rotate(rotateOpts) + if err != nil { + return toExitError(err) + } + + // We open the file *after* the operations on the tree have been + // executed to avoid truncating it when there's errors + if c.Bool("in-place") { + file, err := os.Create(fileName) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + _, err = file.Write(output) + if err != nil { + return toExitError(err) + } + log.Info("File written successfully") + return nil + } + + outputFile := os.Stdout + if c.String("output") != "" { + file, err := os.Create(c.String("output")) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + outputFile = file + } + _, err = outputFile.Write(output) return toExitError(err) - } + }, + }, + { + Name: "edit", + Usage: "edit an encrypted file", + ArgsUsage: `file`, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "kms, k", + Usage: "comma separated list of KMS ARNs", + Sources: cli.EnvVars("SOPS_KMS_ARN"), + }, + &cli.StringFlag{ + Name: "aws-profile", + Usage: "The AWS profile to use for requests to AWS", + }, + &cli.StringFlag{ + Name: "gcp-kms", + Usage: "comma separated list of GCP KMS resource IDs", + Sources: cli.EnvVars("SOPS_GCP_KMS_IDS"), + }, + &cli.StringFlag{ + Name: "hckms", + Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", + Sources: cli.EnvVars("SOPS_HUAWEICLOUD_KMS_IDS"), + }, + &cli.StringFlag{ + Name: "azure-kv", + Usage: "comma separated list of Azure Key Vault URLs", + Sources: cli.EnvVars("SOPS_AZURE_KEYVAULT_URLS"), + }, + &cli.StringFlag{ + Name: "hc-vault-transit", + Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')", + Sources: cli.EnvVars("SOPS_VAULT_URIS"), + }, + &cli.StringFlag{ + Name: "pgp, p", + Usage: "comma separated list of PGP fingerprints", + Sources: cli.EnvVars("SOPS_PGP_FP"), + }, + &cli.StringFlag{ + Name: "age, a", + Usage: "comma separated list of age recipients", + Sources: cli.EnvVars("SOPS_AGE_RECIPIENTS"), + }, + &cli.StringFlag{ + Name: "input-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.StringFlag{ + Name: "unencrypted-suffix", + Usage: "override the unencrypted key suffix.", + }, + &cli.StringFlag{ + Name: "encrypted-suffix", + Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.", + }, + &cli.StringFlag{ + Name: "unencrypted-regex", + Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", + }, + &cli.StringFlag{ + Name: "encrypted-regex", + Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", + }, + &cli.StringFlag{ + Name: "encryption-context", + Usage: "comma separated list of KMS encryption context key:value pairs", + }, + &cli.IntFlag{ + Name: "shamir-secret-sharing-threshold", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, + &cli.BoolFlag{ + Name: "show-master-keys, s", + Usage: "display master encryption keys in the file during editing", + }, + &cli.BoolFlag{ + Name: "ignore-mac", + Usage: "ignore Message Authentication Code during decryption", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) + } + if c.NArg() < 1 { + return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + } + warnMoreThanOnePositionalArgument(c) + fileName, err := filepath.Abs(c.Args().First()) + if err != nil { + return toExitError(err) + } - // We open the file *after* the operations on the tree have been - // executed to avoid truncating it when there's errors - if c.Bool("in-place") { + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) + } + svcs := keyservices(c) + + order, err := decryptionOrder(c.String("decryption-order")) + if err != nil { + return toExitError(err) + } + var output []byte + _, statErr := os.Stat(fileName) + fileExists := statErr == nil + opts := editOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + KeyServices: svcs, + DecryptionOrder: order, + IgnoreMAC: c.Bool("ignore-mac"), + ShowMasterKeys: c.Bool("show-master-keys"), + } + if fileExists { + output, err = edit(opts) + if err != nil { + return toExitError(err) + } + } else { + // File doesn't exist, edit the example file instead + encConfig, err := getEncryptConfig(c, fileName, inputStore, nil) + if err != nil { + return toExitError(err) + } + output, err = editExample(editExampleOpts{ + editOpts: opts, + encryptConfig: encConfig, + }) + if err != nil { + return toExitError(err) + } + } + + // We open the file *after* the operations on the tree have been + // executed to avoid truncating it when there's errors file, err := os.Create(fileName) if err != nil { return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) @@ -1262,385 +1439,593 @@ func main() { } log.Info("File written successfully") return nil - } + }, + }, + { + Name: "set", + Usage: `set a specific key or branch in the input document. value must be a JSON encoded string, for example '/path/to/file ["somekey"][0] {"somevalue":true}', or a path if --value-file is used, or omitted if --value-stdin is used`, + ArgsUsage: `file index [ value ]`, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "input-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.BoolFlag{ + Name: "value-file", + Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-stdin", + }, + &cli.BoolFlag{ + Name: "value-stdin", + Usage: "read the value from stdin; the 'value' argument to 'set' is not needed in this case (avoids leaking secrets in process listings). Mutually exclusive with --value-file", + }, + &cli.IntFlag{ + Name: "shamir-secret-sharing-threshold", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, + &cli.BoolFlag{ + Name: "ignore-mac", + Usage: "ignore Message Authentication Code during decryption", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + &cli.BoolFlag{ + Name: "idempotent", + Usage: "do nothing if the given index already has the given value", + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) + } + if c.Bool("value-file") && c.Bool("value-stdin") { + return common.NewExitError("Error: cannot use both --value-file and --value-stdin", codes.ErrorGeneric) + } + if c.Bool("value-stdin") { + if c.NArg() != 2 { + return common.NewExitError("Error: file specified, or index and value are missing. Need precisely 2 positional arguments since --value-stdin is used.", codes.NoFileSpecified) + } + } else { + if c.NArg() != 3 { + return common.NewExitError("Error: no file specified, or index and value are missing. Need precisely 3 positional arguments.", codes.NoFileSpecified) + } + } + fileName, err := filepath.Abs(c.Args().First()) + if err != nil { + return toExitError(err) + } - outputFile := os.Stdout - if c.String("output") != "" { - file, err := os.Create(c.String("output")) + inputStore, err := inputStore(c, fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + return toExitError(err) } - defer file.Close() - outputFile = file - } - _, err = outputFile.Write(output) - return toExitError(err) - }, - }, - { - Name: "edit", - Usage: "edit an encrypted file", - ArgsUsage: `file`, - Flags: append([]cli.Flag{ - cli.StringFlag{ - Name: "kms, k", - Usage: "comma separated list of KMS ARNs", - EnvVar: "SOPS_KMS_ARN", - }, - cli.StringFlag{ - Name: "aws-profile", - Usage: "The AWS profile to use for requests to AWS", - }, - cli.StringFlag{ - Name: "gcp-kms", - Usage: "comma separated list of GCP KMS resource IDs", - EnvVar: "SOPS_GCP_KMS_IDS", - }, - cli.StringFlag{ - Name: "hckms", - Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", - EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS", - }, - cli.StringFlag{ - Name: "azure-kv", - Usage: "comma separated list of Azure Key Vault URLs", - EnvVar: "SOPS_AZURE_KEYVAULT_URLS", - }, - cli.StringFlag{ - Name: "hc-vault-transit", - Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')", - EnvVar: "SOPS_VAULT_URIS", - }, - cli.StringFlag{ - Name: "pgp, p", - Usage: "comma separated list of PGP fingerprints", - EnvVar: "SOPS_PGP_FP", - }, - cli.StringFlag{ - Name: "age, a", - Usage: "comma separated list of age recipients", - EnvVar: "SOPS_AGE_RECIPIENTS", - }, - cli.StringFlag{ - Name: "input-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.StringFlag{ - Name: "unencrypted-suffix", - Usage: "override the unencrypted key suffix.", - }, - cli.StringFlag{ - Name: "encrypted-suffix", - Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.", - }, - cli.StringFlag{ - Name: "unencrypted-regex", - Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", - }, - cli.StringFlag{ - Name: "encrypted-regex", - Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", - }, - cli.StringFlag{ - Name: "encryption-context", - Usage: "comma separated list of KMS encryption context key:value pairs", - }, - cli.IntFlag{ - Name: "shamir-secret-sharing-threshold", - Usage: "the number of master keys required to retrieve the data key with shamir", - }, - cli.BoolFlag{ - Name: "show-master-keys, s", - Usage: "display master encryption keys in the file during editing", - }, - cli.BoolFlag{ - Name: "ignore-mac", - Usage: "ignore Message Authentication Code during decryption", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", - }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.Bool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) - } - warnMoreThanOnePositionalArgument(c) - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) + } + svcs := keyservices(c) - inputStore, err := inputStore(c, fileName) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, fileName) - if err != nil { - return toExitError(err) - } - svcs := keyservices(c) + path, err := parseTreePath(c.Args().Get(1)) + if err != nil { + return common.NewExitError("Invalid set index format", codes.ErrorInvalidSetFormat) + } - order, err := decryptionOrder(c.String("decryption-order")) - if err != nil { - return toExitError(err) - } - var output []byte - _, statErr := os.Stat(fileName) - fileExists := statErr == nil - opts := editOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - KeyServices: svcs, - DecryptionOrder: order, - IgnoreMAC: c.Bool("ignore-mac"), - ShowMasterKeys: c.Bool("show-master-keys"), - } - if fileExists { - output, err = edit(opts) + var data string + if c.Bool("value-stdin") { + content, err := io.ReadAll(os.Stdin) + if err != nil { + return toExitError(err) + } + data = string(content) + } else if c.Bool("value-file") { + filename := c.Args().Get(2) + content, err := os.ReadFile(filename) + if err != nil { + return toExitError(err) + } + data = string(content) + } else { + data = c.Args().Get(2) + } + value, err := jsonValueToTreeInsertableValue(data) if err != nil { return toExitError(err) } - } else { - // File doesn't exist, edit the example file instead - encConfig, err := getEncryptConfig(c, fileName, inputStore, nil) + + order, err := decryptionOrder(c.String("decryption-order")) if err != nil { return toExitError(err) } - output, err = editExample(editExampleOpts{ - editOpts: opts, - encryptConfig: encConfig, + output, changed, err := set(setOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + KeyServices: svcs, + DecryptionOrder: order, + IgnoreMAC: c.Bool("ignore-mac"), + Value: value, + TreePath: path, }) if err != nil { return toExitError(err) } - } - // We open the file *after* the operations on the tree have been - // executed to avoid truncating it when there's errors - file, err := os.Create(fileName) - if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) - } - defer file.Close() - _, err = file.Write(output) - if err != nil { - return toExitError(err) - } - log.Info("File written successfully") - return nil - }, - }, - { - Name: "set", - Usage: `set a specific key or branch in the input document. value must be a JSON encoded string, for example '/path/to/file ["somekey"][0] {"somevalue":true}', or a path if --value-file is used, or omitted if --value-stdin is used`, - ArgsUsage: `file index [ value ]`, - Flags: append([]cli.Flag{ - cli.StringFlag{ - Name: "input-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.BoolFlag{ - Name: "value-file", - Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-stdin", - }, - cli.BoolFlag{ - Name: "value-stdin", - Usage: "read the value from stdin; the 'value' argument to 'set' is not needed in this case (avoids leaking secrets in process listings). Mutually exclusive with --value-file", - }, - cli.IntFlag{ - Name: "shamir-secret-sharing-threshold", - Usage: "the number of master keys required to retrieve the data key with shamir", - }, - cli.BoolFlag{ - Name: "ignore-mac", - Usage: "ignore Message Authentication Code during decryption", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", - }, - cli.BoolFlag{ - Name: "idempotent", - Usage: "do nothing if the given index already has the given value", + if !changed && c.Bool("idempotent") { + log.Info("File not written due to no change") + return nil + } + + // We open the file *after* the operations on the tree have been + // executed to avoid truncating it when there's errors + file, err := os.Create(fileName) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + _, err = file.Write(output) + if err != nil { + return toExitError(err) + } + log.Info("File written successfully") + return nil }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.Bool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - if c.Bool("value-file") && c.Bool("value-stdin") { - return common.NewExitError("Error: cannot use both --value-file and --value-stdin", codes.ErrorGeneric) - } - if c.Bool("value-stdin") { + }, + { + Name: "unset", + Usage: `unset a specific key or branch in the input document.`, + ArgsUsage: `file index`, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "input-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.IntFlag{ + Name: "shamir-secret-sharing-threshold", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, + &cli.BoolFlag{ + Name: "ignore-mac", + Usage: "ignore Message Authentication Code during decryption", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + &cli.BoolFlag{ + Name: "idempotent", + Usage: "do nothing if the given index does not exist", + }, + }, keyserviceFlags...), + Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) + } if c.NArg() != 2 { - return common.NewExitError("Error: file specified, or index and value are missing. Need precisely 2 positional arguments since --value-stdin is used.", codes.NoFileSpecified) + return common.NewExitError("Error: no file specified, or index is missing", codes.NoFileSpecified) } - } else { - if c.NArg() != 3 { - return common.NewExitError("Error: no file specified, or index and value are missing. Need precisely 3 positional arguments.", codes.NoFileSpecified) + fileName, err := filepath.Abs(c.Args().First()) + if err != nil { + return toExitError(err) + } + + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) } + svcs := keyservices(c) + + path, err := parseTreePath(c.Args().Get(1)) + if err != nil { + return common.NewExitError("Invalid unset index format", codes.ErrorInvalidSetFormat) + } + + order, err := decryptionOrder(c.String("decryption-order")) + if err != nil { + return toExitError(err) + } + output, err := unset(unsetOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + KeyServices: svcs, + DecryptionOrder: order, + IgnoreMAC: c.Bool("ignore-mac"), + TreePath: path, + }) + if err != nil { + if _, ok := err.(*sops.SopsKeyNotFound); ok && c.Bool("idempotent") { + return nil + } + return toExitError(err) + } + + // We open the file *after* the operations on the tree have been + // executed to avoid truncating it when there's errors + file, err := os.Create(fileName) + if err != nil { + return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + } + defer file.Close() + _, err = file.Write(output) + if err != nil { + return toExitError(err) + } + log.Info("File written successfully") + return nil + }, + }, + }, + + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + Name: "decrypt, d", + Usage: "decrypt a file and output the result to stdout", + }, + &cli.BoolFlag{ + Name: "encrypt, e", + Usage: "encrypt a file and output the result to stdout", + }, + &cli.BoolFlag{ + Name: "rotate, r", + Usage: "generate a new data encryption key and reencrypt all values with the new key", + }, + &cli.BoolFlag{ + Name: "disable-version-check", + Usage: "do not check whether the current version is latest during --version", + Sources: cli.EnvVars("SOPS_DISABLE_VERSION_CHECK"), + }, + &cli.BoolFlag{ + Name: "check-for-updates", + Usage: "do check whether the current version is latest during --version", + }, + &cli.StringFlag{ + Name: "kms, k", + Usage: "comma separated list of KMS ARNs", + Sources: cli.EnvVars("SOPS_KMS_ARN"), + }, + &cli.StringFlag{ + Name: "aws-profile", + Usage: "The AWS profile to use for requests to AWS", + }, + &cli.StringFlag{ + Name: "gcp-kms", + Usage: "comma separated list of GCP KMS resource IDs", + Sources: cli.EnvVars("SOPS_GCP_KMS_IDS"), + }, + &cli.StringFlag{ + Name: "hckms", + Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", + Sources: cli.EnvVars("SOPS_HUAWEICLOUD_KMS_IDS"), + }, + &cli.StringFlag{ + Name: "azure-kv", + Usage: "comma separated list of Azure Key Vault URLs", + Sources: cli.EnvVars("SOPS_AZURE_KEYVAULT_URLS"), + }, + &cli.StringFlag{ + Name: "hc-vault-transit", + Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')", + Sources: cli.EnvVars("SOPS_VAULT_URIS"), + }, + &cli.StringFlag{ + Name: "pgp, p", + Usage: "comma separated list of PGP fingerprints", + Sources: cli.EnvVars("SOPS_PGP_FP"), + }, + &cli.StringFlag{ + Name: "age, a", + Usage: "comma separated list of age recipients", + Sources: cli.EnvVars("SOPS_AGE_RECIPIENTS"), + }, + &cli.BoolFlag{ + Name: "in-place, i", + Usage: "write output back to the same file instead of stdout", + }, + &cli.StringFlag{ + Name: "extract", + Usage: "extract a specific key or branch from the input document. Decrypt mode only. Example: --extract '[\"somekey\"][0]'", + }, + &cli.StringFlag{ + Name: "input-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + &cli.StringFlag{ + Name: "output-type", + Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", + }, + &cli.BoolFlag{ + Name: "show-master-keys, s", + Usage: "display master encryption keys in the file during editing", + }, + &cli.StringFlag{ + Name: "add-gcp-kms", + Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-gcp-kms", + Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-hckms", + Usage: "add the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-hckms", + Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-azure-kv", + Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-azure-kv", + Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-kms", + Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-kms", + Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-hc-vault-transit", + Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", + }, + &cli.StringFlag{ + Name: "rm-hc-vault-transit", + Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", + }, + &cli.StringFlag{ + Name: "add-age", + Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-age", + Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "add-pgp", + Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file", + }, + &cli.StringFlag{ + Name: "rm-pgp", + Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file", + }, + &cli.BoolFlag{ + Name: "ignore-mac", + Usage: "ignore Message Authentication Code during decryption", + }, + &cli.BoolFlag{ + Name: "mac-only-encrypted", + Usage: "compute MAC only over values which end up encrypted", + }, + &cli.StringFlag{ + Name: "unencrypted-suffix", + Usage: "override the unencrypted key suffix.", + }, + &cli.StringFlag{ + Name: "encrypted-suffix", + Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.", + }, + &cli.StringFlag{ + Name: "unencrypted-regex", + Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", + }, + &cli.StringFlag{ + Name: "encrypted-regex", + Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", + }, + &cli.StringFlag{ + Name: "unencrypted-comment-regex", + Usage: "set the unencrypted comment suffix. When specified, only keys that have comment matching the regex will be left unencrypted.", + }, + &cli.StringFlag{ + Name: "encrypted-comment-regex", + Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.", + }, + &cli.StringFlag{ + Name: "config", + Usage: "path to sops' config file. If set, sops will not search for the config file recursively.", + Sources: cli.EnvVars("SOPS_CONFIG"), + }, + &cli.StringFlag{ + Name: "encryption-context", + Usage: "comma separated list of KMS encryption context key:value pairs", + }, + &cli.StringFlag{ + Name: "set", + Usage: `set a specific key or branch in the input document. value must be a json encoded string. (edit mode only). eg. --set '["somekey"][0] {"somevalue":true}'`, + }, + &cli.IntFlag{ + Name: "shamir-secret-sharing-threshold", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, + &cli.IntFlag{ + Name: "indent", + Usage: "the number of spaces to indent YAML or JSON encoded file", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Enable verbose logging output", + }, + &cli.StringFlag{ + Name: "output", + Usage: "Save the output after encryption or decryption to the file specified", + }, + &cli.StringFlag{ + Name: "filename-override", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", + }, + &cli.StringFlag{ + Name: "decryption-order", + Usage: "comma separated list of decryption key types", + Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), + }, + }, keyserviceFlags...), + + Action: func(ctx context.Context, c *cli.Command) error { + isDecryptMode := c.Bool("decrypt") + isEncryptMode := c.Bool("encrypt") + isRotateMode := c.Bool("rotate") + isSetMode := c.String("set") != "" + isEditMode := !isEncryptMode && !isDecryptMode && !isRotateMode && !isSetMode + + if c.Bool("verbose") { + logging.SetLevel(logrus.DebugLevel) + } + if c.NArg() < 1 { + return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + } + warnMoreThanOnePositionalArgument(c) + if c.Bool("in-place") && c.String("output") != "" { + return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) + } + fileName, err := filepath.Abs(c.Args().First()) + if err != nil { + return toExitError(err) + } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || + c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { + return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use `--kms` and `--pgp` instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) + if isEncryptMode || isDecryptMode || isRotateMode { + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) } - - inputStore, err := inputStore(c, fileName) + } + fileNameOverride := c.String("filename-override") + if fileNameOverride == "" { + fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) if err != nil { return toExitError(err) } - outputStore, err := outputStore(c, fileName) + } + + commandCount := 0 + if isDecryptMode { + commandCount++ + } + if isEncryptMode { + commandCount++ + } + if isRotateMode { + commandCount++ + } + if isSetMode { + commandCount++ + } + if commandCount > 1 { + log.Warn("More than one command (--encrypt, --decrypt, --rotate, --set) has been specified. Only the changes made by the last one will be visible. Note that this behavior is deprecated and will cause an error eventually.") + } + + // Load configuration here for backwards compatibility (error out in case of bad config files), + // but only when not just decrypting (https://github.com/getsops/sops/issues/868) + needsCreationRule := isEncryptMode || isRotateMode || isSetMode || isEditMode + var config *config.Config + if needsCreationRule { + kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) + config, err = loadConfig(c, fileNameOverride, kmsEncryptionContext) if err != nil { return toExitError(err) } - svcs := keyservices(c) + } - path, err := parseTreePath(c.Args()[1]) - if err != nil { - return common.NewExitError("Invalid set index format", codes.ErrorInvalidSetFormat) - } + inputStore, err := inputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + svcs := keyservices(c) - var data string - if c.Bool("value-stdin") { - content, err := io.ReadAll(os.Stdin) - if err != nil { - return toExitError(err) - } - data = string(content) - } else if c.Bool("value-file") { - filename := c.Args()[2] - content, err := os.ReadFile(filename) - if err != nil { - return toExitError(err) - } - data = string(content) - } else { - data = c.Args()[2] - } - value, err := jsonValueToTreeInsertableValue(data) + order, err := decryptionOrder(c.String("decryption-order")) + if err != nil { + return toExitError(err) + } + var output []byte + if isEncryptMode { + encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config) if err != nil { return toExitError(err) } + output, err = encrypt(encryptOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + KeyServices: svcs, + encryptConfig: encConfig, + }) + // While this check is also done below, the `err` in this scope shadows + // the `err` in the outer scope. **Only** do this in case --decrypt, + // --rotate-, and --set are not specified, though, to keep old behavior. + if err != nil && !isDecryptMode && !isRotateMode && !isSetMode { + return toExitError(err) + } + } - order, err := decryptionOrder(c.String("decryption-order")) + if isDecryptMode { + var extract []interface{} + extract, err = parseTreePath(c.String("extract")) if err != nil { - return toExitError(err) + return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) } - output, changed, err := set(setOpts{ + output, err = decrypt(decryptOpts{ OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, Cipher: aes.NewCipher(), + Extract: extract, KeyServices: svcs, DecryptionOrder: order, IgnoreMAC: c.Bool("ignore-mac"), - Value: value, - TreePath: path, }) + } + if isRotateMode { + rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order) if err != nil { return toExitError(err) } - if !changed && c.Bool("idempotent") { - log.Info("File not written due to no change") - return nil - } - - // We open the file *after* the operations on the tree have been - // executed to avoid truncating it when there's errors - file, err := os.Create(fileName) - if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) - } - defer file.Close() - _, err = file.Write(output) - if err != nil { - return toExitError(err) - } - log.Info("File written successfully") - return nil - }, - }, - { - Name: "unset", - Usage: `unset a specific key or branch in the input document.`, - ArgsUsage: `file index`, - Flags: append([]cli.Flag{ - cli.StringFlag{ - Name: "input-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.IntFlag{ - Name: "shamir-secret-sharing-threshold", - Usage: "the number of master keys required to retrieve the data key with shamir", - }, - cli.BoolFlag{ - Name: "ignore-mac", - Usage: "ignore Message Authentication Code during decryption", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", - }, - cli.BoolFlag{ - Name: "idempotent", - Usage: "do nothing if the given index does not exist", - }, - }, keyserviceFlags...), - Action: func(c *cli.Context) error { - if c.Bool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - if c.NArg() != 2 { - return common.NewExitError("Error: no file specified, or index is missing", codes.NoFileSpecified) - } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } - - inputStore, err := inputStore(c, fileName) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, fileName) + output, err = rotate(rotateOpts) + // While this check is also done below, the `err` in this scope shadows + // the `err` in the outer scope if err != nil { return toExitError(err) } - svcs := keyservices(c) - - path, err := parseTreePath(c.Args()[1]) - if err != nil { - return common.NewExitError("Invalid unset index format", codes.ErrorInvalidSetFormat) - } + } - order, err := decryptionOrder(c.String("decryption-order")) + if isSetMode { + var path []interface{} + var value interface{} + path, value, err = extractSetArguments(c.String("set")) if err != nil { return toExitError(err) } - output, err := unset(unsetOpts{ + output, _, err = set(setOpts{ OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, @@ -1648,17 +2033,51 @@ func main() { KeyServices: svcs, DecryptionOrder: order, IgnoreMAC: c.Bool("ignore-mac"), + Value: value, TreePath: path, }) - if err != nil { - if _, ok := err.(*sops.SopsKeyNotFound); ok && c.Bool("idempotent") { - return nil + } + + if isEditMode { + _, statErr := os.Stat(fileName) + fileExists := statErr == nil + opts := editOpts{ + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + KeyServices: svcs, + DecryptionOrder: order, + IgnoreMAC: c.Bool("ignore-mac"), + ShowMasterKeys: c.Bool("show-master-keys"), + } + if fileExists { + output, err = edit(opts) + } else { + // File doesn't exist, edit the example file instead + encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config) + if err != nil { + return toExitError(err) + } + output, err = editExample(editExampleOpts{ + editOpts: opts, + encryptConfig: encConfig, + }) + // While this check is also done below, the `err` in this scope shadows + // the `err` in the outer scope + if err != nil { + return toExitError(err) } - return toExitError(err) } + } + + if err != nil { + return toExitError(err) + } - // We open the file *after* the operations on the tree have been - // executed to avoid truncating it when there's errors + // We open the file *after* the operations on the tree have been + // executed to avoid truncating it when there's errors + if c.Bool("in-place") || isEditMode || isSetMode { file, err := os.Create(fileName) if err != nil { return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) @@ -1670,446 +2089,29 @@ func main() { } log.Info("File written successfully") return nil - }, - }, - } - app.Flags = append([]cli.Flag{ - cli.BoolFlag{ - Name: "decrypt, d", - Usage: "decrypt a file and output the result to stdout", - }, - cli.BoolFlag{ - Name: "encrypt, e", - Usage: "encrypt a file and output the result to stdout", - }, - cli.BoolFlag{ - Name: "rotate, r", - Usage: "generate a new data encryption key and reencrypt all values with the new key", - }, - cli.BoolFlag{ - Name: "disable-version-check", - Usage: "do not check whether the current version is latest during --version", - EnvVar: "SOPS_DISABLE_VERSION_CHECK", - }, - cli.BoolFlag{ - Name: "check-for-updates", - Usage: "do check whether the current version is latest during --version", - }, - cli.StringFlag{ - Name: "kms, k", - Usage: "comma separated list of KMS ARNs", - EnvVar: "SOPS_KMS_ARN", - }, - cli.StringFlag{ - Name: "aws-profile", - Usage: "The AWS profile to use for requests to AWS", - }, - cli.StringFlag{ - Name: "gcp-kms", - Usage: "comma separated list of GCP KMS resource IDs", - EnvVar: "SOPS_GCP_KMS_IDS", - }, - cli.StringFlag{ - Name: "hckms", - Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", - EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS", - }, - cli.StringFlag{ - Name: "azure-kv", - Usage: "comma separated list of Azure Key Vault URLs", - EnvVar: "SOPS_AZURE_KEYVAULT_URLS", - }, - cli.StringFlag{ - Name: "hc-vault-transit", - Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')", - EnvVar: "SOPS_VAULT_URIS", - }, - cli.StringFlag{ - Name: "pgp, p", - Usage: "comma separated list of PGP fingerprints", - EnvVar: "SOPS_PGP_FP", - }, - cli.StringFlag{ - Name: "age, a", - Usage: "comma separated list of age recipients", - EnvVar: "SOPS_AGE_RECIPIENTS", - }, - cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", - }, - cli.StringFlag{ - Name: "extract", - Usage: "extract a specific key or branch from the input document. Decrypt mode only. Example: --extract '[\"somekey\"][0]'", - }, - cli.StringFlag{ - Name: "input-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", - }, - cli.StringFlag{ - Name: "output-type", - Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", - }, - cli.BoolFlag{ - Name: "show-master-keys, s", - Usage: "display master encryption keys in the file during editing", - }, - cli.StringFlag{ - Name: "add-gcp-kms", - Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-gcp-kms", - Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-hckms", - Usage: "add the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-hckms", - Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-azure-kv", - Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-azure-kv", - Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-kms", - Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-kms", - Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-hc-vault-transit", - Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", - }, - cli.StringFlag{ - Name: "rm-hc-vault-transit", - Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", - }, - cli.StringFlag{ - Name: "add-age", - Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-age", - Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "add-pgp", - Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file", - }, - cli.StringFlag{ - Name: "rm-pgp", - Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file", - }, - cli.BoolFlag{ - Name: "ignore-mac", - Usage: "ignore Message Authentication Code during decryption", - }, - cli.BoolFlag{ - Name: "mac-only-encrypted", - Usage: "compute MAC only over values which end up encrypted", - }, - cli.StringFlag{ - Name: "unencrypted-suffix", - Usage: "override the unencrypted key suffix.", - }, - cli.StringFlag{ - Name: "encrypted-suffix", - Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.", - }, - cli.StringFlag{ - Name: "unencrypted-regex", - Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", - }, - cli.StringFlag{ - Name: "encrypted-regex", - Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", - }, - cli.StringFlag{ - Name: "unencrypted-comment-regex", - Usage: "set the unencrypted comment suffix. When specified, only keys that have comment matching the regex will be left unencrypted.", - }, - cli.StringFlag{ - Name: "encrypted-comment-regex", - Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.", - }, - cli.StringFlag{ - Name: "config", - Usage: "path to sops' config file. If set, sops will not search for the config file recursively.", - EnvVar: "SOPS_CONFIG", - }, - cli.StringFlag{ - Name: "encryption-context", - Usage: "comma separated list of KMS encryption context key:value pairs", - }, - cli.StringFlag{ - Name: "set", - Usage: `set a specific key or branch in the input document. value must be a json encoded string. (edit mode only). eg. --set '["somekey"][0] {"somevalue":true}'`, - }, - cli.IntFlag{ - Name: "shamir-secret-sharing-threshold", - Usage: "the number of master keys required to retrieve the data key with shamir", - }, - cli.IntFlag{ - Name: "indent", - Usage: "the number of spaces to indent YAML or JSON encoded file", - }, - cli.BoolFlag{ - Name: "verbose", - Usage: "Enable verbose logging output", - }, - cli.StringFlag{ - Name: "output", - Usage: "Save the output after encryption or decryption to the file specified", - }, - cli.StringFlag{ - Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", - }, - cli.StringFlag{ - Name: "decryption-order", - Usage: "comma separated list of decryption key types", - EnvVar: "SOPS_DECRYPTION_ORDER", - }, - }, keyserviceFlags...) - - app.Action = func(c *cli.Context) error { - isDecryptMode := c.Bool("decrypt") - isEncryptMode := c.Bool("encrypt") - isRotateMode := c.Bool("rotate") - isSetMode := c.String("set") != "" - isEditMode := !isEncryptMode && !isDecryptMode && !isRotateMode && !isSetMode - - if c.Bool("verbose") { - logging.SetLevel(logrus.DebugLevel) - } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) - } - warnMoreThanOnePositionalArgument(c) - if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) - } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || - c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { - return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use `--kms` and `--pgp` instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) - } - if isEncryptMode || isDecryptMode || isRotateMode { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) - } - } - fileNameOverride := c.String("filename-override") - if fileNameOverride == "" { - fileNameOverride = fileName - } else { - fileNameOverride, err = filepath.Abs(fileNameOverride) - if err != nil { - return toExitError(err) - } - } - - commandCount := 0 - if isDecryptMode { - commandCount++ - } - if isEncryptMode { - commandCount++ - } - if isRotateMode { - commandCount++ - } - if isSetMode { - commandCount++ - } - if commandCount > 1 { - log.Warn("More than one command (--encrypt, --decrypt, --rotate, --set) has been specified. Only the changes made by the last one will be visible. Note that this behavior is deprecated and will cause an error eventually.") - } - - // Load configuration here for backwards compatibility (error out in case of bad config files), - // but only when not just decrypting (https://github.com/getsops/sops/issues/868) - needsCreationRule := isEncryptMode || isRotateMode || isSetMode || isEditMode - var config *config.Config - if needsCreationRule { - kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) - config, err = loadConfig(c, fileNameOverride, kmsEncryptionContext) - if err != nil { - return toExitError(err) } - } - - inputStore, err := inputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - outputStore, err := outputStore(c, fileNameOverride) - if err != nil { - return toExitError(err) - } - svcs := keyservices(c) - - order, err := decryptionOrder(c.String("decryption-order")) - if err != nil { - return toExitError(err) - } - var output []byte - if isEncryptMode { - encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config) - if err != nil { - return toExitError(err) - } - output, err = encrypt(encryptOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - KeyServices: svcs, - encryptConfig: encConfig, - }) - // While this check is also done below, the `err` in this scope shadows - // the `err` in the outer scope. **Only** do this in case --decrypt, - // --rotate-, and --set are not specified, though, to keep old behavior. - if err != nil && !isDecryptMode && !isRotateMode && !isSetMode { - return toExitError(err) - } - } - - if isDecryptMode { - var extract []interface{} - extract, err = parseTreePath(c.String("extract")) - if err != nil { - return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) - } - output, err = decrypt(decryptOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - Extract: extract, - KeyServices: svcs, - DecryptionOrder: order, - IgnoreMAC: c.Bool("ignore-mac"), - }) - } - if isRotateMode { - rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order) - if err != nil { - return toExitError(err) - } - - output, err = rotate(rotateOpts) - // While this check is also done below, the `err` in this scope shadows - // the `err` in the outer scope - if err != nil { - return toExitError(err) - } - } - - if isSetMode { - var path []interface{} - var value interface{} - path, value, err = extractSetArguments(c.String("set")) - if err != nil { - return toExitError(err) - } - output, _, err = set(setOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - KeyServices: svcs, - DecryptionOrder: order, - IgnoreMAC: c.Bool("ignore-mac"), - Value: value, - TreePath: path, - }) - } - if isEditMode { - _, statErr := os.Stat(fileName) - fileExists := statErr == nil - opts := editOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - KeyServices: svcs, - DecryptionOrder: order, - IgnoreMAC: c.Bool("ignore-mac"), - ShowMasterKeys: c.Bool("show-master-keys"), - } - if fileExists { - output, err = edit(opts) - } else { - // File doesn't exist, edit the example file instead - encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config) - if err != nil { - return toExitError(err) - } - output, err = editExample(editExampleOpts{ - editOpts: opts, - encryptConfig: encConfig, - }) - // While this check is also done below, the `err` in this scope shadows - // the `err` in the outer scope + outputFile := os.Stdout + if c.String("output") != "" { + file, err := os.Create(c.String("output")) if err != nil { - return toExitError(err) + return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) } + defer file.Close() + outputFile = file } - } - - if err != nil { + _, err = outputFile.Write(output) return toExitError(err) - } - - // We open the file *after* the operations on the tree have been - // executed to avoid truncating it when there's errors - if c.Bool("in-place") || isEditMode || isSetMode { - file, err := os.Create(fileName) - if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) - } - defer file.Close() - _, err = file.Write(output) - if err != nil { - return toExitError(err) - } - log.Info("File written successfully") - return nil - } - - outputFile := os.Stdout - if c.String("output") != "" { - file, err := os.Create(c.String("output")) - if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) - } - defer file.Close() - outputFile = file - } - _, err = outputFile.Write(output) - return toExitError(err) + }, } - err := app.Run(os.Args) + + err := cmd.Run(context.Background(), os.Args) if err != nil { log.Fatal(err) } } -func getEncryptConfig(c *cli.Context, fileName string, inputStore common.Store, optionalConfig *config.Config) (encryptConfig, error) { +func getEncryptConfig(c *cli.Command, fileName string, inputStore common.Store, optionalConfig *config.Config) (encryptConfig, error) { unencryptedSuffix := c.String("unencrypted-suffix") encryptedSuffix := c.String("encrypted-suffix") encryptedRegex := c.String("encrypted-regex") @@ -2235,7 +2237,7 @@ func getEncryptConfig(c *cli.Context, fileName string, inputStore common.Store, }, nil } -func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, hckmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) { +func getMasterKeys(c *cli.Command, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, hckmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) { var masterKeys []keys.MasterKey for _, k := range kms.MasterKeysFromArnString(c.String(kmsOptionName), kmsEncryptionContext, c.String("aws-profile")) { masterKeys = append(masterKeys, k) @@ -2277,7 +2279,7 @@ func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsO return masterKeys, nil } -func getRotateOpts(c *cli.Context, fileName string, inputStore common.Store, outputStore common.Store, svcs []keyservice.KeyServiceClient, decryptionOrder []string) (rotateOpts, error) { +func getRotateOpts(c *cli.Command, fileName string, inputStore common.Store, outputStore common.Store, svcs []keyservice.KeyServiceClient, decryptionOrder []string) (rotateOpts, error) { kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-hckms", "add-azure-kv", "add-hc-vault-transit", "add-age") if err != nil { @@ -2301,17 +2303,17 @@ func getRotateOpts(c *cli.Context, fileName string, inputStore common.Store, out } func toExitError(err error) error { - if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil { + if cliErr, ok := err.(cli.ExitCoder); ok && cliErr != nil { return cliErr } else if execErr, ok := err.(*osExec.ExitError); ok && execErr != nil { - return cli.NewExitError(err, execErr.ExitCode()) + return cli.Exit(err, execErr.ExitCode()) } else if err != nil { - return cli.NewExitError(err, codes.ErrorGeneric) + return cli.Exit(err, codes.ErrorGeneric) } return nil } -func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) { +func keyservices(c *cli.Command) (svcs []keyservice.KeyServiceClient) { if c.Bool("enable-local-keyservice") { svcs = append(svcs, keyservice.NewLocalClient()) } @@ -2362,8 +2364,8 @@ func findConfigFile() (string, error) { return result.Path, err } -func loadStoresConfig(context *cli.Context, path string) (*config.StoresConfig, error) { - configPath := context.GlobalString("config") +func loadStoresConfig(c *cli.Command, path string) (*config.StoresConfig, error) { + configPath := c.String("config") if configPath == "" { // Ignore config not found errors returned from findConfigFile since the config file is not mandatory foundPath, err := findConfigFile() @@ -2375,27 +2377,27 @@ func loadStoresConfig(context *cli.Context, path string) (*config.StoresConfig, return config.LoadStoresConfig(configPath) } -func inputStore(context *cli.Context, path string) (common.Store, error) { - storesConf, err := loadStoresConfig(context, path) +func inputStore(c *cli.Command, path string) (common.Store, error) { + storesConf, err := loadStoresConfig(c, path) if err != nil { return nil, err } - return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("input-type")), nil + return common.DefaultStoreForPathOrFormat(storesConf, path, c.String("input-type")), nil } -func outputStore(context *cli.Context, path string) (common.Store, error) { - storesConf, err := loadStoresConfig(context, path) +func outputStore(c *cli.Command, path string) (common.Store, error) { + storesConf, err := loadStoresConfig(c, path) if err != nil { return nil, err } - if context.IsSet("indent") { - indent := context.Int("indent") + if c.IsSet("indent") { + indent := c.Int("indent") storesConf.YAML.Indent = indent storesConf.JSON.Indent = indent storesConf.JSONBinary.Indent = indent } - return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("output-type")), nil + return common.DefaultStoreForPathOrFormat(storesConf, path, c.String("output-type")), nil } func parseTreePath(arg string) ([]interface{}, error) { @@ -2425,7 +2427,7 @@ func parseTreePath(arg string) ([]interface{}, error) { return path, nil } -func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]sops.KeyGroup, error) { +func keyGroups(c *cli.Command, file string, optionalConfig *config.Config) ([]sops.KeyGroup, error) { var kmsKeys []keys.MasterKey var pgpKeys []keys.MasterKey var cloudKmsKeys []keys.MasterKey @@ -2518,9 +2520,9 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so // loadConfig will look for an existing config file, either provided through the command line, or using findConfigFile // Since a config file is not required, this function does not error when one is not found, and instead returns a nil config pointer -func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*string) (*config.Config, error) { +func loadConfig(c *cli.Command, file string, kmsEncryptionContext map[string]*string) (*config.Config, error) { var err error - configPath := c.GlobalString("config") + configPath := c.String("config") if configPath == "" { // Ignore config not found errors returned from findConfigFile since the config file is not mandatory configPath, err = findConfigFile() @@ -2536,7 +2538,7 @@ func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*st return conf, nil } -func shamirThreshold(c *cli.Context, file string, optionalConfig *config.Config) (int, error) { +func shamirThreshold(c *cli.Command, file string, optionalConfig *config.Config) (int, error) { if c.Int("shamir-secret-sharing-threshold") != 0 { return c.Int("shamir-secret-sharing-threshold"), nil } diff --git a/go.mod b/go.mod index 5bfcf8453c..e8b24a258b 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 - github.com/urfave/cli v1.22.17 + github.com/urfave/cli/v3 v3.6.1 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.46.0 golang.org/x/net v0.48.0 @@ -87,7 +87,6 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/containerd/continuity v0.4.5 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v28.0.4+incompatible // indirect github.com/docker/docker v28.0.4+incompatible // indirect @@ -131,7 +130,6 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect diff --git a/go.sum b/go.sum index 5ea6bf7ce6..f1a581aa23 100644 --- a/go.sum +++ b/go.sum @@ -52,7 +52,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= @@ -124,8 +123,6 @@ github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -305,8 +302,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= @@ -317,21 +312,18 @@ github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xI github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= -github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= -github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= +github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= +github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= @@ -491,7 +483,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/version/version.go b/version/version.go index d738c4f043..0fbaf5e3c5 100644 --- a/version/version.go +++ b/version/version.go @@ -8,7 +8,7 @@ import ( "github.com/blang/semver" "github.com/hashicorp/go-cleanhttp" - "github.com/urfave/cli" + "github.com/urfave/cli/v3" ) // Version represents the value of the current semantic version. @@ -24,10 +24,10 @@ var Version = "3.11.0" // the latest version from the GitHub API and compare it to the // current version. If the latest version is newer, the function // will print a message to stdout. -func PrintVersion(c *cli.Context) { +func PrintVersion(c *cli.Command) { out := strings.Builder{} - out.WriteString(fmt.Sprintf("%s %s", c.App.Name, c.App.Version)) + out.WriteString(fmt.Sprintf("%s %s", c.Name, c.Version)) if c.Bool("disable-version-check") && !c.Bool("check-for-updates") { out.WriteString("\n") @@ -54,7 +54,7 @@ func PrintVersion(c *cli.Context) { " This will hide this deprecation warning and will always check, even if the default behavior changes in the future.\n") } } - fmt.Fprintf(c.App.Writer, "%s", out.String()) + fmt.Fprintf(c.Writer, "%s", out.String()) } // AIsNewerThanB compares two semantic versions and returns true if A is newer From c4de548c227c13b7f638eebbd50db32897bcda05 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Sun, 12 Oct 2025 10:47:00 +0700 Subject: [PATCH 2/7] chore: NewExitError() -> Exit() to match the underlying cli.Exit() function it calls. Signed-off-by: Hoang Nguyen --- cmd/sops/common/common.go | 28 ++--- cmd/sops/decrypt.go | 6 +- cmd/sops/edit.go | 28 ++--- cmd/sops/encrypt.go | 12 +- cmd/sops/main.go | 112 +++++++++---------- cmd/sops/rotate.go | 2 +- cmd/sops/set.go | 2 +- cmd/sops/subcommand/publish/publish.go | 2 +- cmd/sops/subcommand/updatekeys/updatekeys.go | 4 +- cmd/sops/unset.go | 2 +- 10 files changed, 99 insertions(+), 99 deletions(-) diff --git a/cmd/sops/common/common.go b/cmd/sops/common/common.go index 6a2c05a3ec..5dfba84a76 100644 --- a/cmd/sops/common/common.go +++ b/cmd/sops/common/common.go @@ -85,23 +85,23 @@ type DecryptTreeOpts struct { func DecryptTree(opts DecryptTreeOpts) (dataKey []byte, err error) { dataKey, err = opts.Tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder) if err != nil { - return nil, NewExitError(err, codes.CouldNotRetrieveKey) + return nil, Exit(err, codes.CouldNotRetrieveKey) } computedMac, err := opts.Tree.Decrypt(dataKey, opts.Cipher) if err != nil { - return nil, NewExitError(fmt.Sprintf("Error decrypting tree: %s", err), codes.ErrorDecryptingTree) + return nil, Exit(fmt.Sprintf("Error decrypting tree: %s", err), codes.ErrorDecryptingTree) } fileMac, err := opts.Cipher.Decrypt(opts.Tree.Metadata.MessageAuthenticationCode, dataKey, opts.Tree.Metadata.LastModified.Format(time.RFC3339)) if !opts.IgnoreMac { if err != nil { - return nil, NewExitError(fmt.Sprintf("Cannot decrypt MAC: %s", err), codes.MacMismatch) + return nil, Exit(fmt.Sprintf("Cannot decrypt MAC: %s", err), codes.MacMismatch) } if fileMac != computedMac { // If the file has an empty MAC, display "no MAC" instead of not displaying anything if fileMac == "" { fileMac = "no MAC" } - return nil, NewExitError(fmt.Sprintf("MAC mismatch. File has %s, computed %s", fileMac, computedMac), codes.MacMismatch) + return nil, Exit(fmt.Sprintf("MAC mismatch. File has %s, computed %s", fileMac, computedMac), codes.MacMismatch) } } return dataKey, nil @@ -121,12 +121,12 @@ type EncryptTreeOpts struct { func EncryptTree(opts EncryptTreeOpts) error { unencryptedMac, err := opts.Tree.Encrypt(opts.DataKey, opts.Cipher) if err != nil { - return NewExitError(fmt.Sprintf("Error encrypting tree: %s", err), codes.ErrorEncryptingTree) + return Exit(fmt.Sprintf("Error encrypting tree: %s", err), codes.ErrorEncryptingTree) } opts.Tree.Metadata.LastModified = time.Now().UTC() opts.Tree.Metadata.MessageAuthenticationCode, err = opts.Cipher.Encrypt(unencryptedMac, opts.DataKey, opts.Tree.Metadata.LastModified.Format(time.RFC3339)) if err != nil { - return NewExitError(fmt.Sprintf("Could not encrypt MAC: %s", err), codes.ErrorEncryptingMac) + return Exit(fmt.Sprintf("Could not encrypt MAC: %s", err), codes.ErrorEncryptingMac) } return nil } @@ -138,12 +138,12 @@ func LoadEncryptedFileEx(loader sops.EncryptedFileLoader, inputPath string, read if readFromStdin { fileBytes, err = io.ReadAll(os.Stdin) if err != nil { - return nil, NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) + return nil, Exit(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) } } else { fileBytes, err = os.ReadFile(inputPath) if err != nil { - return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + return nil, Exit(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) } } path, err := filepath.Abs(inputPath) @@ -160,11 +160,11 @@ func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops return LoadEncryptedFileEx(loader, inputPath, false) } -// NewExitError returns a cli.ExitCoder given an error (wrapped in a generic interface{}) +// Exit returns a cli.ExitCoder given an error (wrapped in a generic interface{}) // and an exit code to represent the failure -func NewExitError(i interface{}, exitCode int) cli.ExitCoder { +func Exit(i interface{}, exitCode int) cli.ExitCoder { if userErr, ok := i.(sops.UserError); ok { - return NewExitError(userErr.UserError(), exitCode) + return Exit(userErr.UserError(), exitCode) } return cli.Exit(i, exitCode) } @@ -312,7 +312,7 @@ func FixAWSKMSEncryptionContextBug(opts GenericDecryptOpts, tree *sops.Tree) (*s } if dataKey == nil { - return nil, NewExitError(fmt.Sprintf("Failed to decrypt, meaning there is likely another problem from the encryption context bug: %s", err), codes.ErrorDecryptingTree) + return nil, Exit(fmt.Sprintf("Failed to decrypt, meaning there is likely another problem from the encryption context bug: %s", err), codes.ErrorDecryptingTree) } errs := tree.Metadata.UpdateMasterKeysWithKeyServices(dataKey, opts.KeyServices) @@ -337,12 +337,12 @@ func FixAWSKMSEncryptionContextBug(opts GenericDecryptOpts, tree *sops.Tree) (*s encryptedFile, err := opts.InputStore.EmitEncryptedFile(*tree) if err != nil { - return nil, NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } file, err := os.Create(opts.InputPath) if err != nil { - return nil, NewExitError(fmt.Sprintf("Could not open file for writing: %s", err), codes.CouldNotWriteOutputFile) + return nil, Exit(fmt.Sprintf("Could not open file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(encryptedFile) diff --git a/cmd/sops/decrypt.go b/cmd/sops/decrypt.go index d0da0ddf18..42437b9efa 100644 --- a/cmd/sops/decrypt.go +++ b/cmd/sops/decrypt.go @@ -67,7 +67,7 @@ func decrypt(opts decryptOpts) (decryptedFile []byte, err error) { err = fmt.Errorf("%s\n\n%s", err.Error(), notBinaryHint) } if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error dumping file: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Error dumping file: %s", err), codes.ErrorDumpingTree) } return decryptedFile, err } @@ -84,7 +84,7 @@ func extract(tree *sops.Tree, path []interface{}, outputStore sops.Store) (outpu err = fmt.Errorf("%s\n\n%s", err.Error(), notBinaryHint) } if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error dumping file: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Error dumping file: %s", err), codes.ErrorDumpingTree) } return decrypted, err } else if str, ok := v.(string); ok { @@ -92,7 +92,7 @@ func extract(tree *sops.Tree, path []interface{}, outputStore sops.Store) (outpu } bytes, err := outputStore.EmitValue(v) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error dumping tree: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Error dumping tree: %s", err), codes.ErrorDumpingTree) } return bytes, nil } diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index 3510441533..d4f5a772b9 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -47,7 +47,7 @@ func editExample(opts editExampleOpts) ([]byte, error) { fileBytes := opts.InputStore.EmitExample() branches, err := opts.InputStore.LoadPlainFile(fileBytes) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error unmarshalling file: %s", err), codes.CouldNotReadInputFile) + return nil, common.Exit(fmt.Sprintf("Error unmarshalling file: %s", err), codes.CouldNotReadInputFile) } path, err := filepath.Abs(opts.InputPath) if err != nil { @@ -62,7 +62,7 @@ func editExample(opts editExampleOpts) ([]byte, error) { // Generate a data key dataKey, errs := tree.GenerateDataKeyWithKeyServices(opts.KeyServices) if len(errs) > 0 { - return nil, common.NewExitError(fmt.Sprintf("Error encrypting the data key with one or more master keys: %s", errs), codes.CouldNotRetrieveKey) + return nil, common.Exit(fmt.Sprintf("Error encrypting the data key with one or more master keys: %s", errs), codes.CouldNotRetrieveKey) } return editTree(opts.editOpts, &tree, dataKey) @@ -99,19 +99,19 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) { // Create temporary file for editing tmpdir, err := os.MkdirTemp("", "") if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not create temporary directory: %s", err), codes.CouldNotWriteOutputFile) + return nil, common.Exit(fmt.Sprintf("Could not create temporary directory: %s", err), codes.CouldNotWriteOutputFile) } defer os.RemoveAll(tmpdir) tmpfile, err := os.Create(filepath.Join(tmpdir, filepath.Base(opts.InputPath))) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not create temporary file: %s", err), codes.CouldNotWriteOutputFile) + return nil, common.Exit(fmt.Sprintf("Could not create temporary file: %s", err), codes.CouldNotWriteOutputFile) } // Ensure that in any case, the temporary file is always closed. defer tmpfile.Close() // Ensure that the file is read+write for owner only. if err = tmpfile.Chmod(0600); err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not change permissions of temporary file to read-write for owner only: %s", err), codes.CouldNotWriteOutputFile) + return nil, common.Exit(fmt.Sprintf("Could not change permissions of temporary file to read-write for owner only: %s", err), codes.CouldNotWriteOutputFile) } tmpfileName := tmpfile.Name() @@ -124,17 +124,17 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) { out, err = opts.OutputStore.EmitPlainFile(tree.Branches) } if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } _, err = tmpfile.Write(out) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not write output file: %s", err), codes.CouldNotWriteOutputFile) + return nil, common.Exit(fmt.Sprintf("Could not write output file: %s", err), codes.CouldNotWriteOutputFile) } // Compute file hash to detect if the file has been edited origHash, err := hashFile(tmpfileName) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not hash file: %s", err), codes.CouldNotReadInputFile) + return nil, common.Exit(fmt.Sprintf("Could not hash file: %s", err), codes.CouldNotReadInputFile) } // Close the temporary file, so that an editor can open it. @@ -164,7 +164,7 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) { // Output the file encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } return encryptedFile, nil } @@ -173,18 +173,18 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { for { err := runEditor(opts.TmpFileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not run editor: %s", err), codes.NoEditorFound) + return common.Exit(fmt.Sprintf("Could not run editor: %s", err), codes.NoEditorFound) } newHash, err := hashFile(opts.TmpFileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not hash file: %s", err), codes.CouldNotReadInputFile) + return common.Exit(fmt.Sprintf("Could not hash file: %s", err), codes.CouldNotReadInputFile) } if bytes.Equal(newHash, opts.OriginalHash) { - return common.NewExitError("File has not changed, exiting.", codes.FileHasNotBeenModified) + return common.Exit("File has not changed, exiting.", codes.FileHasNotBeenModified) } edited, err := os.ReadFile(opts.TmpFileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not read edited file: %s", err), codes.CouldNotReadInputFile) + return common.Exit(fmt.Sprintf("Could not read edited file: %s", err), codes.CouldNotReadInputFile) } newBranches, err := opts.InputStore.LoadPlainFile(edited) if err != nil { @@ -217,7 +217,7 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { opts.Tree.Branches = newBranches needVersionUpdated, err := version.AIsNewerThanB(version.Version, opts.Tree.Metadata.Version) if err != nil { - return common.NewExitError(fmt.Sprintf("Failed to compare document version %q with program version %q: %v", opts.Tree.Metadata.Version, version.Version, err), codes.FailedToCompareVersions) + return common.Exit(fmt.Sprintf("Failed to compare document version %q with program version %q: %v", opts.Tree.Metadata.Version, version.Version, err), codes.FailedToCompareVersions) } if needVersionUpdated { opts.Tree.Metadata.Version = version.Version diff --git a/cmd/sops/encrypt.go b/cmd/sops/encrypt.go index e5ffc6950e..6fbbd828cc 100644 --- a/cmd/sops/encrypt.go +++ b/cmd/sops/encrypt.go @@ -84,23 +84,23 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) { if opts.ReadFromStdin { fileBytes, err = io.ReadAll(os.Stdin) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) + return nil, common.Exit(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) } } else { fileBytes, err = os.ReadFile(opts.InputPath) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + return nil, common.Exit(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) } } branches, err := opts.InputStore.LoadPlainFile(fileBytes) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error unmarshalling file: %s", err), codes.CouldNotReadInputFile) + return nil, common.Exit(fmt.Sprintf("Error unmarshalling file: %s", err), codes.CouldNotReadInputFile) } if len(branches) < 1 { - return nil, common.NewExitError("File cannot be completely empty, it must contain at least one document", codes.NeedAtLeastOneDocument) + return nil, common.Exit("File cannot be completely empty, it must contain at least one document", codes.NeedAtLeastOneDocument) } if err := ensureNoMetadata(opts, branches[0]); err != nil { - return nil, common.NewExitError(err, codes.FileAlreadyEncrypted) + return nil, common.Exit(err, codes.FileAlreadyEncrypted) } path, err := filepath.Abs(opts.InputPath) if err != nil { @@ -128,7 +128,7 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) { encryptedFile, err = opts.OutputStore.EmitEncryptedFile(tree) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } return } diff --git a/cmd/sops/main.go b/cmd/sops/main.go index d96c643f67..ccb3618fee 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -211,7 +211,7 @@ For more information, see the README at https://github.com/getsops/sops`, }, keyserviceFlags...), Action: func(ctx context.Context, c *cli.Command) error { if c.NArg() != 2 { - return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) + return common.Exit(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) } fileName := c.Args().First() @@ -242,7 +242,7 @@ For more information, see the README at https://github.com/getsops/sops`, log.Warn("exec-env's --background option is deprecated and will be removed in a future version of sops") if c.Bool("same-process") { - return common.NewExitError("Error: The --same-process flag cannot be used with --background", codes.ErrorConflictingParameters) + return common.Exit("Error: The --same-process flag cannot be used with --background", codes.ErrorConflictingParameters) } } @@ -325,7 +325,7 @@ For more information, see the README at https://github.com/getsops/sops`, }, keyserviceFlags...), Action: func(ctx context.Context, c *cli.Command) error { if c.NArg() != 2 { - return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) + return common.Exit(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric) } fileName := c.Args().First() @@ -417,11 +417,11 @@ For more information, see the README at https://github.com/getsops/sops`, } else { configPath, err = findConfigFile() if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + return common.Exit(err, codes.ErrorGeneric) } } if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + return common.Exit("Error: no file specified", codes.NoFileSpecified) } warnMoreThanOnePositionalArgument(c) path := c.Args().First() @@ -460,7 +460,7 @@ For more information, see the README at https://github.com/getsops/sops`, if cliErr, ok := err.(cli.ExitCoder); ok && cliErr != nil { return cliErr } else if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + return common.Exit(err, codes.ErrorGeneric) } } return nil @@ -522,7 +522,7 @@ For more information, see the README at https://github.com/getsops/sops`, }, Action: func(ctx context.Context, c *cli.Command) error { if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + return common.Exit("Error: no file specified", codes.NoFileSpecified) } fileName := c.Args().First() @@ -542,7 +542,7 @@ For more information, see the README at https://github.com/getsops/sops`, json, err := encodingjson.Marshal(status) if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + return common.Exit(err, codes.ErrorGeneric) } fmt.Println(string(json)) @@ -615,7 +615,7 @@ For more information, see the README at https://github.com/getsops/sops`, azkvs := c.StringSlice("azure-kv") ageRecipients := c.StringSlice("age") if c.NArg() != 0 { - return common.NewExitError(fmt.Errorf("error: no positional arguments allowed"), codes.ErrorGeneric) + return common.Exit(fmt.Errorf("error: no positional arguments allowed"), codes.ErrorGeneric) } var group sops.KeyGroup for _, fp := range pgpFps { @@ -693,7 +693,7 @@ For more information, see the README at https://github.com/getsops/sops`, Action: func(ctx context.Context, c *cli.Command) error { if c.NArg() != 1 { - return common.NewExitError(fmt.Errorf("error: exactly one positional argument (index) required"), codes.ErrorGeneric) + return common.Exit(fmt.Errorf("error: exactly one positional argument (index) required"), codes.ErrorGeneric) } group, err := strconv.ParseUint(c.Args().First(), 10, 32) if err != nil { @@ -743,11 +743,11 @@ For more information, see the README at https://github.com/getsops/sops`, } else { configPath, err = findConfigFile() if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + return common.Exit(err, codes.ErrorGeneric) } } if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + return common.Exit("Error: no file specified", codes.NoFileSpecified) } failedCounter := 0 for _, path := range c.Args().Slice() { @@ -765,7 +765,7 @@ For more information, see the README at https://github.com/getsops/sops`, if cliErr, ok := err.(cli.ExitCoder); ok && cliErr != nil { return cliErr } else if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + return common.Exit(err, codes.ErrorGeneric) } } @@ -777,7 +777,7 @@ For more information, see the README at https://github.com/getsops/sops`, } } if failedCounter > 0 { - return common.NewExitError(fmt.Errorf("failed updating %d key(s)", failedCounter), codes.ErrorGeneric) + return common.Exit(fmt.Errorf("failed updating %d key(s)", failedCounter), codes.ErrorGeneric) } return nil }, @@ -827,11 +827,11 @@ For more information, see the README at https://github.com/getsops/sops`, } readFromStdin := c.NArg() == 0 if readFromStdin && c.Bool("in-place") { - return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) + return common.Exit("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) + return common.Exit("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } var fileName string var err error @@ -841,7 +841,7 @@ For more information, see the README at https://github.com/getsops/sops`, return toExitError(err) } if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + return common.Exit(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) } } fileNameOverride := c.String("filename-override") @@ -872,7 +872,7 @@ For more information, see the README at https://github.com/getsops/sops`, var extract []interface{} extract, err = parseTreePath(c.String("extract")) if err != nil { - return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) + return common.Exit(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) } output, err := decrypt(decryptOpts{ OutputStore: outputStore, @@ -894,7 +894,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.Bool("in-place") { file, err := os.Create(fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(output) @@ -909,7 +909,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.String("output") != "" { file, err := os.Create(c.String("output")) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() outputFile = file @@ -1014,15 +1014,15 @@ For more information, see the README at https://github.com/getsops/sops`, readFromStdin := c.NArg() == 0 if readFromStdin { if c.Bool("in-place") { - return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) + return common.Exit("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) } if c.String("filename-override") == "" { - return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters) + return common.Exit("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters) } } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) + return common.Exit("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } var fileName string var err error @@ -1032,7 +1032,7 @@ For more information, see the README at https://github.com/getsops/sops`, return toExitError(err) } if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + return common.Exit(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) } } fileNameOverride := c.String("filename-override") @@ -1077,7 +1077,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.Bool("in-place") { file, err := os.Create(fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(output) @@ -1092,7 +1092,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.String("output") != "" { file, err := os.Create(c.String("output")) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() outputFile = file @@ -1197,11 +1197,11 @@ For more information, see the README at https://github.com/getsops/sops`, logging.SetLevel(logrus.DebugLevel) } if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + return common.Exit("Error: no file specified", codes.NoFileSpecified) } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) + return common.Exit("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } fileName, err := filepath.Abs(c.Args().First()) if err != nil { @@ -1210,7 +1210,7 @@ For more information, see the README at https://github.com/getsops/sops`, if _, err := os.Stat(fileName); os.IsNotExist(err) { if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { - return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) + return common.Exit(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) } } fileNameOverride := c.String("filename-override") @@ -1252,7 +1252,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.Bool("in-place") { file, err := os.Create(fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(output) @@ -1267,7 +1267,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.String("output") != "" { file, err := os.Create(c.String("output")) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() outputFile = file @@ -1371,7 +1371,7 @@ For more information, see the README at https://github.com/getsops/sops`, logging.SetLevel(logrus.DebugLevel) } if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + return common.Exit("Error: no file specified", codes.NoFileSpecified) } warnMoreThanOnePositionalArgument(c) fileName, err := filepath.Abs(c.Args().First()) @@ -1430,7 +1430,7 @@ For more information, see the README at https://github.com/getsops/sops`, // executed to avoid truncating it when there's errors file, err := os.Create(fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(output) @@ -1485,15 +1485,15 @@ For more information, see the README at https://github.com/getsops/sops`, logging.SetLevel(logrus.DebugLevel) } if c.Bool("value-file") && c.Bool("value-stdin") { - return common.NewExitError("Error: cannot use both --value-file and --value-stdin", codes.ErrorGeneric) + return common.Exit("Error: cannot use both --value-file and --value-stdin", codes.ErrorGeneric) } if c.Bool("value-stdin") { if c.NArg() != 2 { - return common.NewExitError("Error: file specified, or index and value are missing. Need precisely 2 positional arguments since --value-stdin is used.", codes.NoFileSpecified) + return common.Exit("Error: file specified, or index and value are missing. Need precisely 2 positional arguments since --value-stdin is used.", codes.NoFileSpecified) } } else { if c.NArg() != 3 { - return common.NewExitError("Error: no file specified, or index and value are missing. Need precisely 3 positional arguments.", codes.NoFileSpecified) + return common.Exit("Error: no file specified, or index and value are missing. Need precisely 3 positional arguments.", codes.NoFileSpecified) } } fileName, err := filepath.Abs(c.Args().First()) @@ -1513,7 +1513,7 @@ For more information, see the README at https://github.com/getsops/sops`, path, err := parseTreePath(c.Args().Get(1)) if err != nil { - return common.NewExitError("Invalid set index format", codes.ErrorInvalidSetFormat) + return common.Exit("Invalid set index format", codes.ErrorInvalidSetFormat) } var data string @@ -1566,7 +1566,7 @@ For more information, see the README at https://github.com/getsops/sops`, // executed to avoid truncating it when there's errors file, err := os.Create(fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(output) @@ -1613,7 +1613,7 @@ For more information, see the README at https://github.com/getsops/sops`, logging.SetLevel(logrus.DebugLevel) } if c.NArg() != 2 { - return common.NewExitError("Error: no file specified, or index is missing", codes.NoFileSpecified) + return common.Exit("Error: no file specified, or index is missing", codes.NoFileSpecified) } fileName, err := filepath.Abs(c.Args().First()) if err != nil { @@ -1632,7 +1632,7 @@ For more information, see the README at https://github.com/getsops/sops`, path, err := parseTreePath(c.Args().Get(1)) if err != nil { - return common.NewExitError("Invalid unset index format", codes.ErrorInvalidSetFormat) + return common.Exit("Invalid unset index format", codes.ErrorInvalidSetFormat) } order, err := decryptionOrder(c.String("decryption-order")) @@ -1660,7 +1660,7 @@ For more information, see the README at https://github.com/getsops/sops`, // executed to avoid truncating it when there's errors file, err := os.Create(fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(output) @@ -1893,11 +1893,11 @@ For more information, see the README at https://github.com/getsops/sops`, logging.SetLevel(logrus.DebugLevel) } if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + return common.Exit("Error: no file specified", codes.NoFileSpecified) } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { - return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) + return common.Exit("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } fileName, err := filepath.Abs(c.Args().First()) if err != nil { @@ -1906,10 +1906,10 @@ For more information, see the README at https://github.com/getsops/sops`, if _, err := os.Stat(fileName); os.IsNotExist(err) { if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { - return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use `--kms` and `--pgp` instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) + return common.Exit(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use `--kms` and `--pgp` instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) } if isEncryptMode || isDecryptMode || isRotateMode { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + return common.Exit(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) } } fileNameOverride := c.String("filename-override") @@ -1991,7 +1991,7 @@ For more information, see the README at https://github.com/getsops/sops`, var extract []interface{} extract, err = parseTreePath(c.String("extract")) if err != nil { - return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) + return common.Exit(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) } output, err = decrypt(decryptOpts{ OutputStore: outputStore, @@ -2080,7 +2080,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.Bool("in-place") || isEditMode || isSetMode { file, err := os.Create(fileName) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() _, err = file.Write(output) @@ -2095,7 +2095,7 @@ For more information, see the README at https://github.com/getsops/sops`, if c.String("output") != "" { file, err := os.Create(c.String("output")) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) + return common.Exit(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile) } defer file.Close() outputFile = file @@ -2204,7 +2204,7 @@ func getEncryptConfig(c *cli.Command, fileName string, inputStore common.Store, } if cryptRuleCount > 1 { - return encryptConfig{}, common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file", codes.ErrorConflictingParameters) + return encryptConfig{}, common.Exit("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file", codes.ErrorConflictingParameters) } // only supply the default UnencryptedSuffix when EncryptedSuffix, EncryptedRegex, and others are not provided @@ -2437,7 +2437,7 @@ func keyGroups(c *cli.Command, file string, optionalConfig *config.Config) ([]so var ageMasterKeys []keys.MasterKey kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) if c.String("encryption-context") != "" && kmsEncryptionContext == nil { - return nil, common.NewExitError("Invalid KMS encryption context format", codes.ErrorInvalidKMSEncryptionContextFormat) + return nil, common.Exit("Invalid KMS encryption context format", codes.ErrorInvalidKMSEncryptionContextFormat) } if c.String("kms") != "" { for _, k := range kms.MasterKeysFromArnString(c.String("kms"), kmsEncryptionContext, c.String("aws-profile")) { @@ -2560,7 +2560,7 @@ func jsonValueToTreeInsertableValue(jsonValue string) (interface{}, error) { var valueToInsert interface{} err := encodingjson.Unmarshal([]byte(jsonValue), &valueToInsert) if err != nil { - return nil, common.NewExitError("Value for --set is not valid JSON", codes.ErrorInvalidSetFormat) + return nil, common.Exit("Value for --set is not valid JSON", codes.ErrorInvalidSetFormat) } // Check if decoding it as json we find a single value // and not a map or slice, in which case we can't marshal @@ -2570,7 +2570,7 @@ func jsonValueToTreeInsertableValue(jsonValue string) (interface{}, error) { var err error valueToInsert, err = (&json.Store{}).LoadPlainFile([]byte(jsonValue)) if err != nil { - return nil, common.NewExitError("Invalid --set value format", codes.ErrorInvalidSetFormat) + return nil, common.Exit("Invalid --set value format", codes.ErrorInvalidSetFormat) } } // Fix for #461 @@ -2588,20 +2588,20 @@ func extractSetArguments(set string) (path []interface{}, valueToInsert interfac // Since python-dict-index has to end with ], we split at "] " to get the two parts pathValuePair := strings.SplitAfterN(set, "] ", 2) if len(pathValuePair) < 2 { - return nil, nil, common.NewExitError("Invalid --set format", codes.ErrorInvalidSetFormat) + return nil, nil, common.Exit("Invalid --set format", codes.ErrorInvalidSetFormat) } fullPath := strings.TrimRight(pathValuePair[0], " ") jsonValue := pathValuePair[1] valueToInsert, err = jsonValueToTreeInsertableValue(jsonValue) if err != nil { - // All errors returned by jsonValueToTreeInsertableValue are created by common.NewExitError(), + // All errors returned by jsonValueToTreeInsertableValue are created by common.Exit(), // so we can simply pass them on return nil, nil, err } path, err = parseTreePath(fullPath) if err != nil { - return nil, nil, common.NewExitError("Invalid --set format", codes.ErrorInvalidSetFormat) + return nil, nil, common.Exit("Invalid --set format", codes.ErrorInvalidSetFormat) } return path, valueToInsert, nil } @@ -2614,7 +2614,7 @@ func decryptionOrder(decryptionOrder string) ([]string, error) { unique := make(map[string]struct{}) for _, v := range orderList { if _, ok := unique[v]; ok { - return nil, common.NewExitError(fmt.Sprintf("Duplicate decryption key type: %s", v), codes.DuplicateDecryptionKeyType) + return nil, common.Exit(fmt.Sprintf("Duplicate decryption key type: %s", v), codes.DuplicateDecryptionKeyType) } unique[v] = struct{}{} } diff --git a/cmd/sops/rotate.go b/cmd/sops/rotate.go index 072b558e5f..526b9a23db 100644 --- a/cmd/sops/rotate.go +++ b/cmd/sops/rotate.go @@ -83,7 +83,7 @@ func rotate(opts rotateOpts) ([]byte, error) { encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } return encryptedFile, nil } diff --git a/cmd/sops/set.go b/cmd/sops/set.go index 7a7da298df..217d4916e7 100644 --- a/cmd/sops/set.go +++ b/cmd/sops/set.go @@ -60,7 +60,7 @@ func set(opts setOpts) ([]byte, bool, error) { encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree) if err != nil { - return nil, false, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, false, common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } return encryptedFile, changed, err } diff --git a/cmd/sops/subcommand/publish/publish.go b/cmd/sops/subcommand/publish/publish.go index aed1118de5..f204166656 100644 --- a/cmd/sops/subcommand/publish/publish.go +++ b/cmd/sops/subcommand/publish/publish.go @@ -129,7 +129,7 @@ func Run(opts Opts) error { fileContents, err = opts.InputStore.EmitEncryptedFile(*tree) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } } else { fileContents, err = os.ReadFile(path) diff --git a/cmd/sops/subcommand/updatekeys/updatekeys.go b/cmd/sops/subcommand/updatekeys/updatekeys.go index 9dec066a67..d011278985 100644 --- a/cmd/sops/subcommand/updatekeys/updatekeys.go +++ b/cmd/sops/subcommand/updatekeys/updatekeys.go @@ -100,7 +100,7 @@ func updateFile(opts Opts) error { } key, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder) if err != nil { - return common.NewExitError(err, codes.CouldNotRetrieveKey) + return common.Exit(err, codes.CouldNotRetrieveKey) } tree.Metadata.KeyGroups = conf.KeyGroups tree.Metadata.ShamirThreshold = shamirThreshold @@ -110,7 +110,7 @@ func updateFile(opts Opts) error { } output, err := store.EmitEncryptedFile(*tree) if err != nil { - return common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } outputFile, err := os.Create(opts.InputPath) if err != nil { diff --git a/cmd/sops/unset.go b/cmd/sops/unset.go index bb369748c3..3a38893b30 100644 --- a/cmd/sops/unset.go +++ b/cmd/sops/unset.go @@ -61,7 +61,7 @@ func unset(opts unsetOpts) ([]byte, error) { encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, common.Exit(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } return encryptedFile, err } From 2a6ff821133a5e2bad42fc59599284f585962fc5 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Sun, 12 Oct 2025 10:54:17 +0700 Subject: [PATCH 3/7] fix: remove unused parameter path in loadStoresConfig() function Signed-off-by: Hoang Nguyen --- cmd/sops/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index ccb3618fee..e407f3534f 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -2364,7 +2364,7 @@ func findConfigFile() (string, error) { return result.Path, err } -func loadStoresConfig(c *cli.Command, path string) (*config.StoresConfig, error) { +func loadStoresConfig(c *cli.Command) (*config.StoresConfig, error) { configPath := c.String("config") if configPath == "" { // Ignore config not found errors returned from findConfigFile since the config file is not mandatory @@ -2378,7 +2378,7 @@ func loadStoresConfig(c *cli.Command, path string) (*config.StoresConfig, error) } func inputStore(c *cli.Command, path string) (common.Store, error) { - storesConf, err := loadStoresConfig(c, path) + storesConf, err := loadStoresConfig(c) if err != nil { return nil, err } @@ -2386,7 +2386,7 @@ func inputStore(c *cli.Command, path string) (common.Store, error) { } func outputStore(c *cli.Command, path string) (common.Store, error) { - storesConf, err := loadStoresConfig(c, path) + storesConf, err := loadStoresConfig(c) if err != nil { return nil, err } From a0ca60e5c492c8457a64c98a94dcb62dbd98a94a Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Sun, 12 Oct 2025 13:34:59 +0700 Subject: [PATCH 4/7] feat: add manpage subcommand Signed-off-by: Hoang Nguyen --- cmd/sops/main.go | 19 +++++++++++++++++++ go.mod | 5 ++++- go.sum | 6 ++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index e407f3534f..42360604b2 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/sirupsen/logrus" + "github.com/urfave/cli-docs/v3" "github.com/urfave/cli/v3" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -182,6 +183,24 @@ For more information, see the README at https://github.com/getsops/sops`, EnableShellCompletion: true, Commands: []*cli.Command{ + { + Name: "manpage", + Usage: "Output man page for sops command", + Hidden: true, + Action: func(ctx context.Context, c *cli.Command) error { + man, err := docs.ToManWithSection(c.Root(), 1) + if err != nil { + return cli.Exit(err, 1) + } + + _, err = c.Writer.Write([]byte(man)) + if err != nil { + return cli.Exit(err, 1) + } + + return nil + }, + }, { Name: "exec-env", Usage: "execute a command with decrypted values inserted into the environment", diff --git a/go.mod b/go.mod index e8b24a258b..21bed45d67 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/getsops/sops/v3 -go 1.24.0 +go 1.24.4 require ( cloud.google.com/go/kms v1.23.2 @@ -33,6 +33,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 + github.com/urfave/cli-docs/v3 v3.1.0 github.com/urfave/cli/v3 v3.6.1 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.46.0 @@ -87,6 +88,7 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/containerd/continuity v0.4.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v28.0.4+incompatible // indirect github.com/docker/docker v28.0.4+incompatible // indirect @@ -130,6 +132,7 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect diff --git a/go.sum b/go.sum index f1a581aa23..c66b483d1a 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,8 @@ github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -302,6 +304,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= @@ -322,6 +326,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= +github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= From 3b4d7f99e97309ef0ee10d1982ef5ac53a078542 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 13 Oct 2025 10:55:44 +0700 Subject: [PATCH 5/7] fix: properly set alternate flag names using Aliases Signed-off-by: Hoang Nguyen --- cmd/sops/main.go | 116 +++++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 42360604b2..37008cc380 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -404,8 +404,9 @@ For more information, see the README at https://github.com/getsops/sops`, ArgsUsage: `file`, Flags: append([]cli.Flag{ &cli.BoolFlag{ - Name: "yes, y", - Usage: `pre-approve all changes and run non-interactively`, + Name: "yes", + Aliases: []string{"y"}, + Usage: `pre-approve all changes and run non-interactively`, }, &cli.BoolFlag{ Name: "omit-extensions", @@ -495,14 +496,16 @@ For more information, see the README at https://github.com/getsops/sops`, Usage: "start a SOPS key service server", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "network, net", - Usage: "network to listen on, e.g. 'tcp' or 'unix'", - Value: "tcp", + Name: "network", + Aliases: []string{"net"}, + Usage: "network to listen on, e.g. 'tcp' or 'unix'", + Value: "tcp", }, &cli.StringFlag{ - Name: "address, addr", - Usage: "address to listen on, e.g. '127.0.0.1:5000' or '/tmp/sops.sock'", - Value: "127.0.0.1:5000", + Name: "address", + Aliases: []string{"addr"}, + Usage: "address to listen on, e.g. '127.0.0.1:5000' or '/tmp/sops.sock'", + Value: "127.0.0.1:5000", }, &cli.BoolFlag{ Name: "prompt", @@ -578,8 +581,9 @@ For more information, see the README at https://github.com/getsops/sops`, Usage: "add a new group to a SOPS file", Flags: append([]cli.Flag{ &cli.StringFlag{ - Name: "file, f", - Usage: "the file to add the group to", + Name: "file", + Aliases: []string{"f"}, + Usage: "the file to add the group to", }, &cli.StringSliceFlag{ Name: "pgp", @@ -614,8 +618,9 @@ For more information, see the README at https://github.com/getsops/sops`, Usage: "the age recipient the new group should contain. Can be specified more than once", }, &cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", + Name: "in-place", + Aliases: []string{"i"}, + Usage: "write output back to the same file instead of stdout", }, &cli.IntFlag{ Name: "shamir-secret-sharing-threshold", @@ -696,12 +701,14 @@ For more information, see the README at https://github.com/getsops/sops`, Usage: "delete a key group from a SOPS file", Flags: append([]cli.Flag{ &cli.StringFlag{ - Name: "file, f", - Usage: "the file to add the group to", + Name: "file", + Aliases: []string{"f"}, + Usage: "the file to add the group to", }, &cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", + Name: "in-place", + Aliases: []string{"i"}, + Usage: "write output back to the same file instead of stdout", }, &cli.IntFlag{ Name: "shamir-secret-sharing-threshold", @@ -746,8 +753,9 @@ For more information, see the README at https://github.com/getsops/sops`, ArgsUsage: `file`, Flags: append([]cli.Flag{ &cli.BoolFlag{ - Name: "yes, y", - Usage: `pre-approve all changes and run non-interactively`, + Name: "yes", + Aliases: []string{"y"}, + Usage: `pre-approve all changes and run non-interactively`, }, &cli.StringFlag{ Name: "input-type", @@ -807,8 +815,9 @@ For more information, see the README at https://github.com/getsops/sops`, ArgsUsage: `[file]`, Flags: append([]cli.Flag{ &cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", + Name: "in-place", + Aliases: []string{"i"}, + Usage: "write output back to the same file instead of stdout", }, &cli.StringFlag{ Name: "extract", @@ -943,15 +952,17 @@ For more information, see the README at https://github.com/getsops/sops`, ArgsUsage: `[file]`, Flags: append([]cli.Flag{ &cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", + Name: "in-place", + Aliases: []string{"i"}, + Usage: "write output back to the same file instead of stdout", }, &cli.StringFlag{ Name: "output", Usage: "Save the output after encryption to the file specified", }, &cli.StringFlag{ - Name: "kms, k", + Name: "kms", + Aliases: []string{"k"}, Usage: "comma separated list of KMS ARNs", Sources: cli.EnvVars("SOPS_KMS_ARN"), }, @@ -980,12 +991,14 @@ For more information, see the README at https://github.com/getsops/sops`, Sources: cli.EnvVars("SOPS_VAULT_URIS"), }, &cli.StringFlag{ - Name: "pgp, p", + Name: "pgp", + Aliases: []string{"p"}, Usage: "comma separated list of PGP fingerprints", Sources: cli.EnvVars("SOPS_PGP_FP"), }, &cli.StringFlag{ - Name: "age, a", + Name: "age", + Aliases: []string{"a"}, Usage: "comma separated list of age recipients", Sources: cli.EnvVars("SOPS_AGE_RECIPIENTS"), }, @@ -1126,8 +1139,9 @@ For more information, see the README at https://github.com/getsops/sops`, ArgsUsage: `file`, Flags: append([]cli.Flag{ &cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", + Name: "in-place", + Aliases: []string{"i"}, + Usage: "write output back to the same file instead of stdout", }, &cli.StringFlag{ Name: "output", @@ -1301,7 +1315,8 @@ For more information, see the README at https://github.com/getsops/sops`, ArgsUsage: `file`, Flags: append([]cli.Flag{ &cli.StringFlag{ - Name: "kms, k", + Name: "kms", + Aliases: []string{"k"}, Usage: "comma separated list of KMS ARNs", Sources: cli.EnvVars("SOPS_KMS_ARN"), }, @@ -1330,12 +1345,14 @@ For more information, see the README at https://github.com/getsops/sops`, Sources: cli.EnvVars("SOPS_VAULT_URIS"), }, &cli.StringFlag{ - Name: "pgp, p", + Name: "pgp", + Aliases: []string{"p"}, Usage: "comma separated list of PGP fingerprints", Sources: cli.EnvVars("SOPS_PGP_FP"), }, &cli.StringFlag{ - Name: "age, a", + Name: "age", + Aliases: []string{"a"}, Usage: "comma separated list of age recipients", Sources: cli.EnvVars("SOPS_AGE_RECIPIENTS"), }, @@ -1372,8 +1389,9 @@ For more information, see the README at https://github.com/getsops/sops`, Usage: "the number of master keys required to retrieve the data key with shamir", }, &cli.BoolFlag{ - Name: "show-master-keys, s", - Usage: "display master encryption keys in the file during editing", + Name: "show-master-keys", + Aliases: []string{"s"}, + Usage: "display master encryption keys in the file during editing", }, &cli.BoolFlag{ Name: "ignore-mac", @@ -1694,16 +1712,19 @@ For more information, see the README at https://github.com/getsops/sops`, Flags: append([]cli.Flag{ &cli.BoolFlag{ - Name: "decrypt, d", - Usage: "decrypt a file and output the result to stdout", + Name: "decrypt", + Aliases: []string{"d"}, + Usage: "decrypt a file and output the result to stdout", }, &cli.BoolFlag{ - Name: "encrypt, e", - Usage: "encrypt a file and output the result to stdout", + Name: "encrypt", + Aliases: []string{"e"}, + Usage: "encrypt a file and output the result to stdout", }, &cli.BoolFlag{ - Name: "rotate, r", - Usage: "generate a new data encryption key and reencrypt all values with the new key", + Name: "rotate", + Aliases: []string{"r"}, + Usage: "generate a new data encryption key and reencrypt all values with the new key", }, &cli.BoolFlag{ Name: "disable-version-check", @@ -1715,7 +1736,8 @@ For more information, see the README at https://github.com/getsops/sops`, Usage: "do check whether the current version is latest during --version", }, &cli.StringFlag{ - Name: "kms, k", + Name: "kms", + Aliases: []string{"k"}, Usage: "comma separated list of KMS ARNs", Sources: cli.EnvVars("SOPS_KMS_ARN"), }, @@ -1744,18 +1766,21 @@ For more information, see the README at https://github.com/getsops/sops`, Sources: cli.EnvVars("SOPS_VAULT_URIS"), }, &cli.StringFlag{ - Name: "pgp, p", + Name: "pgp", + Aliases: []string{"p"}, Usage: "comma separated list of PGP fingerprints", Sources: cli.EnvVars("SOPS_PGP_FP"), }, &cli.StringFlag{ - Name: "age, a", + Name: "age", + Aliases: []string{"a"}, Usage: "comma separated list of age recipients", Sources: cli.EnvVars("SOPS_AGE_RECIPIENTS"), }, &cli.BoolFlag{ - Name: "in-place, i", - Usage: "write output back to the same file instead of stdout", + Name: "in-place", + Aliases: []string{"i"}, + Usage: "write output back to the same file instead of stdout", }, &cli.StringFlag{ Name: "extract", @@ -1770,8 +1795,9 @@ For more information, see the README at https://github.com/getsops/sops`, Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", }, &cli.BoolFlag{ - Name: "show-master-keys, s", - Usage: "display master encryption keys in the file during editing", + Name: "show-master-keys", + Aliases: []string{"s"}, + Usage: "display master encryption keys in the file during editing", }, &cli.StringFlag{ Name: "add-gcp-kms", From 1971ca719b78f9cb8238f6ee25858e5dcf1140ee Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 13 Oct 2025 14:18:15 +0700 Subject: [PATCH 6/7] fix: remove duplicated --verbose flag declaration for subcommands It's already defined as a root command flag. Signed-off-by: Hoang Nguyen --- cmd/sops/main.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 37008cc380..e842f3eaea 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -416,10 +416,6 @@ For more information, see the README at https://github.com/getsops/sops`, Name: "recursive", Usage: "If the source path is a directory, publish all its content recursively", }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Enable verbose logging output", - }, &cli.StringFlag{ Name: "decryption-order", Usage: "comma separated list of decryption key types", @@ -511,10 +507,6 @@ For more information, see the README at https://github.com/getsops/sops`, Name: "prompt", Usage: "Prompt user to confirm every incoming request", }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Enable verbose logging output", - }, }, Action: func(ctx context.Context, c *cli.Command) error { if c.Bool("verbose") { From 902d08d282bfc83e343eacf6e042370e0aec05b5 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 13 Oct 2025 14:10:50 +0700 Subject: [PATCH 7/7] chore: mark all root command flags as Local With this change, we don't need the Go template hack anymore. The help text for subcommands displays correctly without showing global options. Signed-off-by: Hoang Nguyen --- cmd/sops/main.go | 85 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index e842f3eaea..fb4dc6c586 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -78,24 +78,6 @@ func warnMoreThanOnePositionalArgument(c *cli.Command) { func main() { cli.VersionPrinter = version.PrintVersion - // Remove GLOBAL OPTIONS from the help text of subcommands - cli.CommandHelpTemplate = `NAME: - {{template "helpNameTemplate" .}} - -USAGE: - {{template "usageTemplate" .}}{{if .Category}} - -CATEGORY: - {{.Category}}{{end}}{{if .Description}} - -DESCRIPTION: - {{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}} - -OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} - -OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} -` - keyserviceFlags := []cli.Flag{ &cli.BoolFlag{ Name: "enable-local-keyservice", @@ -1702,222 +1684,285 @@ For more information, see the README at https://github.com/getsops/sops`, }, }, - Flags: append([]cli.Flag{ + Flags: []cli.Flag{ &cli.BoolFlag{ Name: "decrypt", Aliases: []string{"d"}, + Local: true, Usage: "decrypt a file and output the result to stdout", }, &cli.BoolFlag{ Name: "encrypt", Aliases: []string{"e"}, + Local: true, Usage: "encrypt a file and output the result to stdout", }, &cli.BoolFlag{ Name: "rotate", Aliases: []string{"r"}, + Local: true, Usage: "generate a new data encryption key and reencrypt all values with the new key", }, &cli.BoolFlag{ Name: "disable-version-check", + Local: true, Usage: "do not check whether the current version is latest during --version", Sources: cli.EnvVars("SOPS_DISABLE_VERSION_CHECK"), }, &cli.BoolFlag{ Name: "check-for-updates", + Local: true, Usage: "do check whether the current version is latest during --version", }, &cli.StringFlag{ Name: "kms", Aliases: []string{"k"}, + Local: true, Usage: "comma separated list of KMS ARNs", Sources: cli.EnvVars("SOPS_KMS_ARN"), }, &cli.StringFlag{ Name: "aws-profile", + Local: true, Usage: "The AWS profile to use for requests to AWS", }, &cli.StringFlag{ Name: "gcp-kms", + Local: true, Usage: "comma separated list of GCP KMS resource IDs", Sources: cli.EnvVars("SOPS_GCP_KMS_IDS"), }, &cli.StringFlag{ Name: "hckms", + Local: true, Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", Sources: cli.EnvVars("SOPS_HUAWEICLOUD_KMS_IDS"), }, &cli.StringFlag{ Name: "azure-kv", + Local: true, Usage: "comma separated list of Azure Key Vault URLs", Sources: cli.EnvVars("SOPS_AZURE_KEYVAULT_URLS"), }, &cli.StringFlag{ Name: "hc-vault-transit", + Local: true, Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')", Sources: cli.EnvVars("SOPS_VAULT_URIS"), }, &cli.StringFlag{ Name: "pgp", Aliases: []string{"p"}, + Local: true, Usage: "comma separated list of PGP fingerprints", Sources: cli.EnvVars("SOPS_PGP_FP"), }, &cli.StringFlag{ Name: "age", Aliases: []string{"a"}, + Local: true, Usage: "comma separated list of age recipients", Sources: cli.EnvVars("SOPS_AGE_RECIPIENTS"), }, &cli.BoolFlag{ Name: "in-place", Aliases: []string{"i"}, + Local: true, Usage: "write output back to the same file instead of stdout", }, &cli.StringFlag{ Name: "extract", + Local: true, Usage: "extract a specific key or branch from the input document. Decrypt mode only. Example: --extract '[\"somekey\"][0]'", }, &cli.StringFlag{ Name: "input-type", + Local: true, Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", }, &cli.StringFlag{ Name: "output-type", + Local: true, Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", }, &cli.BoolFlag{ Name: "show-master-keys", Aliases: []string{"s"}, + Local: true, Usage: "display master encryption keys in the file during editing", }, &cli.StringFlag{ Name: "add-gcp-kms", + Local: true, Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file", }, &cli.StringFlag{ Name: "rm-gcp-kms", + Local: true, Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file", }, &cli.StringFlag{ Name: "add-hckms", + Local: true, Usage: "add the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) to the list of master keys on the given file", }, &cli.StringFlag{ Name: "rm-hckms", + Local: true, Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file", }, &cli.StringFlag{ Name: "add-azure-kv", + Local: true, Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file", }, &cli.StringFlag{ Name: "rm-azure-kv", + Local: true, Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file", }, &cli.StringFlag{ Name: "add-kms", + Local: true, Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file", }, &cli.StringFlag{ Name: "rm-kms", + Local: true, Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file", }, &cli.StringFlag{ Name: "add-hc-vault-transit", + Local: true, Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", }, &cli.StringFlag{ Name: "rm-hc-vault-transit", + Local: true, Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)", }, &cli.StringFlag{ Name: "add-age", + Local: true, Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file", }, &cli.StringFlag{ Name: "rm-age", + Local: true, Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file", }, &cli.StringFlag{ Name: "add-pgp", + Local: true, Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file", }, &cli.StringFlag{ Name: "rm-pgp", + Local: true, Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file", }, &cli.BoolFlag{ Name: "ignore-mac", + Local: true, Usage: "ignore Message Authentication Code during decryption", }, &cli.BoolFlag{ Name: "mac-only-encrypted", + Local: true, Usage: "compute MAC only over values which end up encrypted", }, &cli.StringFlag{ Name: "unencrypted-suffix", + Local: true, Usage: "override the unencrypted key suffix.", }, &cli.StringFlag{ Name: "encrypted-suffix", + Local: true, Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.", }, &cli.StringFlag{ Name: "unencrypted-regex", + Local: true, Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", }, &cli.StringFlag{ Name: "encrypted-regex", + Local: true, Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", }, &cli.StringFlag{ Name: "unencrypted-comment-regex", + Local: true, Usage: "set the unencrypted comment suffix. When specified, only keys that have comment matching the regex will be left unencrypted.", }, &cli.StringFlag{ Name: "encrypted-comment-regex", + Local: true, Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.", }, &cli.StringFlag{ Name: "config", + Local: true, Usage: "path to sops' config file. If set, sops will not search for the config file recursively.", Sources: cli.EnvVars("SOPS_CONFIG"), }, &cli.StringFlag{ Name: "encryption-context", + Local: true, Usage: "comma separated list of KMS encryption context key:value pairs", }, &cli.StringFlag{ Name: "set", + Local: true, Usage: `set a specific key or branch in the input document. value must be a json encoded string. (edit mode only). eg. --set '["somekey"][0] {"somevalue":true}'`, }, &cli.IntFlag{ Name: "shamir-secret-sharing-threshold", + Local: true, Usage: "the number of master keys required to retrieve the data key with shamir", }, &cli.IntFlag{ Name: "indent", + Local: true, Usage: "the number of spaces to indent YAML or JSON encoded file", }, &cli.BoolFlag{ Name: "verbose", + Local: true, Usage: "Enable verbose logging output", }, &cli.StringFlag{ Name: "output", + Local: true, Usage: "Save the output after encryption or decryption to the file specified", }, &cli.StringFlag{ Name: "filename-override", + Local: true, Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", }, &cli.StringFlag{ Name: "decryption-order", + Local: true, Usage: "comma separated list of decryption key types", Sources: cli.EnvVars("SOPS_DECRYPTION_ORDER"), }, - }, keyserviceFlags...), + // Repeat keyserviceFlags, with Local value set to true + &cli.BoolFlag{ + Name: "enable-local-keyservice", + Value: true, + Local: true, + Usage: "use local key service", + Sources: cli.EnvVars("SOPS_ENABLE_LOCAL_KEYSERVICE"), + }, + &cli.StringSliceFlag{ + Name: "keyservice", + Local: true, + Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000", + Sources: cli.EnvVars("SOPS_KEYSERVICE"), + }, + }, Action: func(ctx context.Context, c *cli.Command) error { isDecryptMode := c.Bool("decrypt")