pulumi/cmd/config.go
Matt Ellis 8f076b7cb3 Argument validation for CLI commands
Previously, we were inconsistent on how we handled argument validation
in the CLI. Many commands used cobra.Command's Args property to
provide a validator if they took arguments, but commands which did not
rarely used cobra.NoArgs to indicate this.

This change does two things:

1. Introduce `cmdutil.ArgsFunc` which works like `cmdutil.RunFunc`, it
wraps an existing cobra type and lets us control the behavior when an
arguments validator fails.

2. Ensure every command sets the Args property with an instance of
cmdutil.ArgsFunc. The cmdutil package defines wrapers for all the
cobra validators we are using, to prevent us from having to spell out
`cmduitl.ArgsFunc(...)` everywhere.

Fixes #588
2017-11-29 16:10:53 -08:00

438 lines
10 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (
"fmt"
"sort"
"strings"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/pack"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
)
func newConfigCmd() *cobra.Command {
var stack string
var showSecrets bool
cmd := &cobra.Command{
Use: "config",
Short: "Manage configuration",
Long: "Lists all configuration values for a specific stack. To add a new configuration value, run\n" +
"'pulumi config set', to remove and existing value run 'pulumi config rm'. To get the value of\n" +
"for a specific configuration key, use 'pulumi config get <key-name>'.",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
stackName, err := explicitOrCurrent(stack, backend)
if err != nil {
return err
}
return listConfig(stackName, showSecrets)
}),
}
cmd.Flags().BoolVar(
&showSecrets, "show-secrets", false,
"Show secret values when listing config instead of displaying blinded values")
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"Operate on a different stack than the currently selected stack")
cmd.AddCommand(newConfigGetCmd(&stack))
cmd.AddCommand(newConfigRmCmd(&stack))
cmd.AddCommand(newConfigSetCmd(&stack))
return cmd
}
func newConfigGetCmd(stack *string) *cobra.Command {
getCmd := &cobra.Command{
Use: "get <key>",
Short: "Get a single configuration value",
Args: cmdutil.ExactArgs(1),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
stackName, err := explicitOrCurrent(*stack, backend)
if err != nil {
return err
}
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
return getConfig(stackName, key)
}),
}
return getCmd
}
func newConfigRmCmd(stack *string) *cobra.Command {
var all bool
var save bool
rmCmd := &cobra.Command{
Use: "rm <key>",
Short: "Remove configuration value",
Args: cmdutil.ExactArgs(1),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
if all && *stack != "" {
return errors.New("if --all is specified, an explicit stack can not be provided")
}
var stackName tokens.QName
if !all {
var err error
if stackName, err = explicitOrCurrent(*stack, backend); err != nil {
return err
}
}
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
if save {
return deleteProjectConfiguration(stackName, key)
}
return deleteWorkspaceConfiguration(stackName, key)
}),
}
rmCmd.PersistentFlags().BoolVar(
&save, "save", false,
"Remove the configuration value in the project file instead instead of a locally set value")
rmCmd.PersistentFlags().BoolVar(
&all, "all", false,
"Remove a project wide configuration value that applies to all stacks")
return rmCmd
}
func newConfigSetCmd(stack *string) *cobra.Command {
var all bool
var save bool
var secret bool
setCmd := &cobra.Command{
Use: "set <key> [value]",
Short: "Set configuration value",
Args: cmdutil.RangeArgs(1, 2),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
if all && *stack != "" {
return errors.New("if --all is specified, an explicit stack can not be provided")
}
var stackName tokens.QName
if !all {
var err error
if stackName, err = explicitOrCurrent(*stack, backend); err != nil {
return err
}
}
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
var c config.ValueEncrypter
if secret {
c, err = getSymmetricCrypter()
if err != nil {
return err
}
}
var value string
if len(args) == 2 {
value = args[1]
} else if !secret {
value, err = readConsole("value")
if err != nil {
return err
}
} else {
value, err = readConsoleNoEchoWithPrompt("value")
if err != nil {
return err
}
}
if !secret {
err = setConfiguration(stackName, key, config.NewValue(value), save)
if err != nil {
return err
}
fmt.Printf("Set key '%s' with value '%s' as plaintext\n", args[0], value)
return nil
}
enc, err := c.EncryptValue(value)
if err != nil {
return err
}
err = setConfiguration(stackName, key, config.NewSecureValue(enc), save)
if err != nil {
return err
}
fmt.Printf("Set key '%s' with with encrypted value\n", args[0])
return nil
}),
}
setCmd.PersistentFlags().BoolVar(
&secret, "secret", false,
"Encrypt the value instead of storing it in plaintext")
setCmd.PersistentFlags().BoolVar(
&save, "save", false,
"Save the configuration value in the project file instead of locally")
setCmd.PersistentFlags().BoolVar(
&all, "all", false,
"Set a configuration value for all stacks for this project")
return setCmd
}
func parseConfigKey(key string) (tokens.ModuleMember, error) {
// As a convience, we'll treat any key with no delimiter as if:
// <program-name>:config:<key> had been written instead
if !strings.Contains(key, tokens.TokenDelimiter) {
pkg, err := getPackage()
if err != nil {
return "", err
}
return tokens.ParseModuleMember(fmt.Sprintf("%s:config:%s", pkg.Name, key))
}
return tokens.ParseModuleMember(key)
}
func prettyKey(key string) string {
pkg, err := getPackage()
if err != nil {
return key
}
return prettyKeyForPackage(key, pkg)
}
func prettyKeyForPackage(key string, pkg *pack.Package) string {
s := key
defaultPrefix := fmt.Sprintf("%s:config:", pkg.Name)
if strings.HasPrefix(s, defaultPrefix) {
return s[len(defaultPrefix):]
}
return s
}
func setConfiguration(stackName tokens.QName, key tokens.ModuleMember, value config.Value, save bool) error {
if save {
return setProjectConfiguration(stackName, key, value)
}
return setWorkspaceConfiguration(stackName, key, value)
}
func listConfig(stackName tokens.QName, showSecrets bool) error {
cfg, err := getConfiguration(stackName)
if err != nil {
return err
}
var decrypter config.ValueDecrypter = blindingDecrypter{}
if hasSecureValue(cfg) && showSecrets {
decrypter, err = getSymmetricCrypter()
if err != nil {
return err
}
}
if cfg != nil {
fmt.Printf("%-32s %-32s\n", "KEY", "VALUE")
var keys []string
for key := range cfg {
// Note that we use the fully qualified module member here instead of a `prettyKey`, this lets us ensure
// that all the config values for the current program are displayed next to one another in the output.
keys = append(keys, string(key))
}
sort.Strings(keys)
for _, key := range keys {
decrypted, err := cfg[tokens.ModuleMember(key)].Value(decrypter)
if err != nil {
return errors.Wrap(err, "could not decrypt configuration value")
}
fmt.Printf("%-32s %-32s\n", prettyKey(key), decrypted)
}
}
return nil
}
func getConfig(stackName tokens.QName, key tokens.ModuleMember) error {
cfg, err := getConfiguration(stackName)
if err != nil {
return err
}
if cfg != nil {
if v, ok := cfg[key]; ok {
var decrypter config.ValueDecrypter = panicCrypter{}
if v.Secure() {
decrypter, err = getSymmetricCrypter()
if err != nil {
return err
}
}
decrypted, err := v.Value(decrypter)
if err != nil {
return errors.Wrap(err, "could not decrypt configuation value")
}
fmt.Printf("%v\n", decrypted)
return nil
}
}
return errors.Errorf("configuration key '%v' not found for stack '%v'", prettyKey(key.String()), stackName)
}
func getConfiguration(stackName tokens.QName) (map[tokens.ModuleMember]config.Value, error) {
contract.Require(stackName != "", "stackName")
workspace, err := newWorkspace()
if err != nil {
return nil, err
}
pkg, err := getPackage()
if err != nil {
return nil, err
}
configs := make([]map[tokens.ModuleMember]config.Value, 4)
configs = append(configs, pkg.Config)
if stackInfo, has := pkg.Stacks[stackName]; has {
configs = append(configs, stackInfo.Config)
}
if localAllStackConfig, has := workspace.Settings().Config[""]; has {
configs = append(configs, localAllStackConfig)
}
if localStackConfig, has := workspace.Settings().Config[stackName]; has {
configs = append(configs, localStackConfig)
}
return mergeConfigs(configs...), nil
}
func deleteProjectConfiguration(stackName tokens.QName, key tokens.ModuleMember) error {
pkg, err := getPackage()
if err != nil {
return err
}
if stackName == "" {
if pkg.Config != nil {
delete(pkg.Config, key)
}
} else {
if pkg.Stacks[stackName].Config != nil {
delete(pkg.Stacks[stackName].Config, key)
}
}
return savePackage(pkg)
}
func deleteWorkspaceConfiguration(stackName tokens.QName, key tokens.ModuleMember) error {
workspace, err := newWorkspace()
if err != nil {
return err
}
if config, has := workspace.Settings().Config[stackName]; has {
delete(config, key)
}
return workspace.Save()
}
func setProjectConfiguration(stackName tokens.QName, key tokens.ModuleMember, value config.Value) error {
pkg, err := getPackage()
if err != nil {
return err
}
if stackName == "" {
if pkg.Config == nil {
pkg.Config = make(map[tokens.ModuleMember]config.Value)
}
pkg.Config[key] = value
} else {
if pkg.Stacks == nil {
pkg.Stacks = make(map[tokens.QName]pack.StackInfo)
}
if pkg.Stacks[stackName].Config == nil {
si := pkg.Stacks[stackName]
si.Config = make(map[tokens.ModuleMember]config.Value)
pkg.Stacks[stackName] = si
}
pkg.Stacks[stackName].Config[key] = value
}
return savePackage(pkg)
}
func setWorkspaceConfiguration(stackName tokens.QName, key tokens.ModuleMember, value config.Value) error {
workspace, err := newWorkspace()
if err != nil {
return err
}
if _, has := workspace.Settings().Config[stackName]; !has {
workspace.Settings().Config[stackName] = make(map[tokens.ModuleMember]config.Value)
}
workspace.Settings().Config[stackName][key] = value
return workspace.Save()
}
func mergeConfigs(configs ...map[tokens.ModuleMember]config.Value) map[tokens.ModuleMember]config.Value {
merged := make(map[tokens.ModuleMember]config.Value)
for _, config := range configs {
for key, value := range config {
merged[key] = value
}
}
return merged
}