pulumi/cmd/config.go
Joe Duffy 776a76dffd
Make some stack-related CLI improvements (#947)
This change includes a handful of stack-related CLI formatting
improvements that I've been noodling on in the background for a while,
based on things that tend to trip up demos and the inner loop workflow.

This includes:

* If `pulumi stack select` is run by itself, use an interactive
  CLI menu to let the user select an existing stack, or choose to
  create a new one.  This looks as follows

      $ pulumi stack select
      Please choose a stack, or choose to create a new one:
        abcdef
        babblabblabble
      > currentlyselected
        defcon
        <create a new stack>

  and is navigated in the usual way (key up, down, enter).

* If a stack name is passed that does not exist, prompt the user
  to ask whether s/he wants to create one on-demand.  This hooks
  interesting moments in time, like `pulumi stack select foo`,
  and cuts down on the need to run additional commands.

* If a current stack is required, but none is currently selected,
  then pop the same interactive menu shown above to select one.
  Depending on the command being run, we may or may not show the
  option to create a new stack (e.g., that doesn't make much sense
  when you're running `pulumi destroy`, but might when you're
  running `pulumi stack`).  This again lets you do with a single
  command what would have otherwise entailed an error with multiple
  commands to recover from it.

* If you run `pulumi stack init` without any additional arguments,
  we interactively prompt for the stack name.  Before, we would
  error and you'd then need to run `pulumi stack init <name>`.

* Colorize some things nicely; for example, now all prompts will
  by default become bright white.
2018-02-16 15:03:54 -08:00

468 lines
12 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (
"fmt"
"io/ioutil"
"os"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/backend/state"
"github.com/pulumi/pulumi/pkg/diag"
"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/util/contract"
"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), true)
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), true)
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, true)
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",
Long: "Configuration values can be accessed when a stack is being deployed and used to configure behavior. \n" +
"If a value is not present on the command line, pulumi will prompt for the value. Multi-line values\n" +
"may be set by piping a file to standard in.",
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")
}
if all && secret {
return errors.New("if --all is specified, the value may not be marked secret")
}
// Ensure the stack exists.
s, err := requireStack(stackName, true)
if err != nil {
return err
}
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
var value string
switch {
case len(args) == 2:
value = args[1]
case !terminal.IsTerminal(int(os.Stdin.Fd())):
b, readerr := ioutil.ReadAll(os.Stdin)
if readerr != nil {
return readerr
}
value = cmdutil.RemoveTralingNewline(string(b))
case secret:
value, err = cmdutil.ReadConsoleNoEcho("value")
if err != nil {
return err
}
default:
value, err = cmdutil.ReadConsole("value")
if err != nil {
return err
}
}
// Encrypt the config value if needed.
var v config.Value
if secret {
c, cerr := backend.GetStackCrypter(s)
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) {
proj, err := workspace.DetectProject()
if err != nil {
return "", err
}
return tokens.ParseModuleMember(fmt.Sprintf("%s:config:%s", proj.Name, key))
}
return tokens.ParseModuleMember(key)
}
func prettyKey(key string) string {
proj, err := workspace.DetectProject()
if err != nil {
return key
}
return prettyKeyForProject(key, proj)
}
func prettyKeyForProject(key string, proj *workspace.Project) string {
s := key
defaultPrefix := fmt.Sprintf("%s:config:", proj.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 = backend.GetStackCrypter(stack)
if err != nil {
return err
}
} else {
decrypter = config.NewBlindingDecrypter()
}
if cfg != nil {
// Devote 48 characters to the config key, unless there's a key longer, in which case use that.
maxkey := 48
for key := range cfg {
if len(key) > maxkey {
maxkey = len(key)
}
}
fmt.Printf("%-"+strconv.Itoa(maxkey)+"s %-48s\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("%-"+strconv.Itoa(maxkey)+"s %-48s\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 = backend.GetStackCrypter(stack); 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 deleteAllStackConfiguration(stackName tokens.QName) error {
contract.Require(stackName != "", "stackName")
w, err := workspace.New()
if err != nil {
return err
}
proj, err := w.Project()
if err != nil {
return err
}
delete(w.Settings().Config, stackName)
err = w.Save()
if err != nil {
return err
}
if info, has := proj.Stacks[stackName]; has {
info.Config = nil
info.EncryptionSalt = ""
proj.Stacks[stackName] = info
}
return workspace.SaveProject(proj)
}
func deleteProjectConfiguration(stackName tokens.QName, key tokens.ModuleMember) error {
proj, err := workspace.DetectProject()
if err != nil {
return err
}
if stackName == "" {
if proj.Config != nil {
delete(proj.Config, key)
}
} else {
if proj.Stacks[stackName].Config != nil {
delete(proj.Stacks[stackName].Config, key)
}
}
return workspace.SaveProject(proj)
}
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 {
proj, err := workspace.DetectProject()
if err != nil {
return err
}
if stackName == "" {
if proj.Config == nil {
proj.Config = make(map[tokens.ModuleMember]config.Value)
}
proj.Config[key] = value
} else {
if proj.Stacks == nil {
proj.Stacks = make(map[tokens.QName]workspace.ProjectStack)
}
if proj.Stacks[stackName].Config == nil {
si := proj.Stacks[stackName]
si.Config = make(map[tokens.ModuleMember]config.Value)
proj.Stacks[stackName] = si
}
proj.Stacks[stackName].Config[key] = value
}
return workspace.SaveProject(proj)
}
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()
}