pulumi/cmd/config.go
Matt Ellis cc04cd6581 Use per stack key for local stacks instead of per project
In the Pulumi Cloud, there is no guarantee that two stacks will share
the same encryption key. This means that encrypted config can not be
shared across stacks (in the Pulumi.yaml) file. To mimic this behavior
in the local experience, we now use a unique key per stack.

When upgrading an existing project, for any stack with existing
secrets, we copy the existing key into this stack. Future stacks will
get thier own encryption key. This strikes a balance between
expediency of implementation, the end user UX and not having to make a
breaking change.

As part of this change, I have introduced a CHANGELOG.md file in the
root of the repository and added a small note about the change to it.

Fixes #769
2018-01-19 00:50:59 -08:00

457 lines
11 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (
"fmt"
"sort"
"strconv"
"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/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))
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")
}
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)
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 := 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) {
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 = 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
}
pkg, err := w.GetPackage()
if err != nil {
return err
}
delete(w.Settings().Config, stackName)
err = w.Save()
if err != nil {
return err
}
if info, has := pkg.Stacks[stackName]; has {
info.Config = nil
info.EncryptionSalt = ""
pkg.Stacks[stackName] = info
}
return workspace.SavePackage(pkg)
}
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()
}