pulumi/cmd/config.go
Joe Duffy 36ab8f0087
Make config a little less error prone
As articulated in #714, the way config defaults to workspace-local
configuration is a bit error prone, especially now with the cloud
workflow being the default.  This change implements several improvements:

* First, --save defaults to true, so that configuration changes will
  persist into your project file.  If you want the old local workspace
  behavior, you can specify --save=false.

* Second, the order in which we applied configuration was a little
  strange, because workspace settings overwrote project settings.
  The order is changed now so that we take most specific over least
  specific configuration.  Per-stack is considered more specific
  than global and project settings are considered more specific
  than workspace.

* We now warn anytime workspace local configuration is used.  This
  is a developer scenario and can have subtle effects.  It is simply
  not safe to use in a team environment.  In fact, I lost an arm
  this morning due to workspace config... and that's why you always
  issue warnings for unsafe things.
2017-12-13 10:46:54 -08:00

414 lines
10 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (
"fmt"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/backend/state"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/pack"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
"github.com/pulumi/pulumi/pkg/workspace"
)
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 {
stack, err := requireStack(tokens.QName(stack))
if err != nil {
return err
}
return listConfig(stack, 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.SpecificArgs([]string{"key"}),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
s, err := requireStack(tokens.QName(*stack))
if err != nil {
return err
}
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
return getConfig(s, 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.SpecificArgs([]string{"key"}),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
stackName := tokens.QName(*stack)
if all && stackName != "" {
return errors.New("if --all is specified, an explicit stack can not be provided")
}
// Ensure the stack exists.
s, err := requireStack(stackName)
if err != nil {
return err
}
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
var stackToSave tokens.QName
if !all {
stackToSave = s.Name()
}
if save {
return deleteProjectConfiguration(stackToSave, key)
}
return deleteWorkspaceConfiguration(stackToSave, key)
}),
}
rmCmd.PersistentFlags().BoolVar(
&all, "all", false,
"Remove a project wide configuration value that applies to all stacks")
rmCmd.PersistentFlags().BoolVar(
&save, "save", true,
"Remove the configuration value from the project file (if false, it is private to your workspace)")
return rmCmd
}
func newConfigSetCmd(stack *string) *cobra.Command {
var all bool
var plaintext 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 {
stackName := tokens.QName(*stack)
if all && stackName != "" {
return errors.New("if --all is specified, an explicit stack can not be provided")
}
// Ensure the stack exists.
s, err := requireStack(stackName)
if err != nil {
return err
}
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
// Read the value from an arg or the console, disabling echoing if a secret.
var value string
if len(args) == 2 {
value = args[1]
} else if secret {
value, err = cmdutil.ReadConsoleNoEcho("value")
if err != nil {
return err
}
} else {
value, err = cmdutil.ReadConsole("value")
if err != nil {
return err
}
}
// Encrypt the config value if needed.
var v config.Value
if secret {
c, cerr := state.SymmetricCrypter()
if cerr != nil {
return cerr
}
enc, eerr := c.EncryptValue(value)
if eerr != nil {
return eerr
}
v = config.NewSecureValue(enc)
} else {
v = config.NewValue(value)
}
// And now save it.
var stackToSave tokens.QName
if !all {
stackToSave = s.Name()
}
err = setConfiguration(stackToSave, key, v, save)
if err != nil {
return err
}
// If we saved a plaintext configuration value, and --plaintext was not passed, warn the user.
if !secret && !plaintext && save {
cmdutil.Diag().Warningf(
diag.Message(
"saved config key '%s' value '%s' as plaintext; "+
"re-run with --secret to encrypt the value instead"),
key, value)
}
return nil
}),
}
setCmd.PersistentFlags().BoolVar(
&all, "all", false,
"Set a configuration value for all stacks for this project")
setCmd.PersistentFlags().BoolVar(
&plaintext, "plaintext", false,
"Save the value as plaintext (unencrypted)")
setCmd.PersistentFlags().BoolVar(
&save, "save", true,
"Save the configuration value in the project file (if false, it is private to your workspace)")
setCmd.PersistentFlags().BoolVar(
&secret, "secret", false,
"Encrypt the value instead of storing it in plaintext")
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 := workspace.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 := workspace.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(stack backend.Stack, showSecrets bool) error {
cfg, err := state.Configuration(cmdutil.Diag(), stack.Name())
if err != nil {
return err
}
// By default, we will use a blinding decrypter to show '******'. If requested, display secrets in plaintext.
var decrypter config.Decrypter
if cfg.HasSecureValue() && showSecrets {
decrypter, err = state.SymmetricCrypter()
if err != nil {
return err
}
} else {
decrypter = config.NewBlindingDecrypter()
}
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(stack backend.Stack, key tokens.ModuleMember) error {
cfg, err := state.Configuration(cmdutil.Diag(), stack.Name())
if err != nil {
return err
}
if cfg != nil {
if v, ok := cfg[key]; ok {
var d config.Decrypter
if v.Secure() {
var err error
if d, err = state.DefaultCrypter(cfg); err != nil {
return errors.Wrap(err, "could not create a decrypter")
}
} else {
d = config.NewPanicCrypter()
}
raw, err := v.Value(d)
if err != nil {
return errors.Wrap(err, "could not decrypt configuation value")
}
fmt.Printf("%v\n", raw)
return nil
}
}
return errors.Errorf(
"configuration key '%v' not found for stack '%v'", prettyKey(key.String()), stack.Name())
}
func deleteProjectConfiguration(stackName tokens.QName, key tokens.ModuleMember) error {
pkg, err := workspace.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 workspace.SavePackage(pkg)
}
func deleteWorkspaceConfiguration(stackName tokens.QName, key tokens.ModuleMember) error {
w, err := workspace.New()
if err != nil {
return err
}
if config, has := w.Settings().Config[stackName]; has {
delete(config, key)
}
return w.Save()
}
func setProjectConfiguration(stackName tokens.QName, key tokens.ModuleMember, value config.Value) error {
pkg, err := workspace.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 workspace.SavePackage(pkg)
}
func setWorkspaceConfiguration(stackName tokens.QName, key tokens.ModuleMember, value config.Value) error {
w, err := workspace.New()
if err != nil {
return err
}
if _, has := w.Settings().Config[stackName]; !has {
w.Settings().Config[stackName] = make(map[tokens.ModuleMember]config.Value)
}
w.Settings().Config[stackName][key] = value
return w.Save()
}