Merge pull request #986 from pulumi/config-refactor
Rework config storage
This commit is contained in:
commit
96d7f9307a
238
cmd/config.go
238
cmd/config.go
|
@ -15,12 +15,10 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
@ -107,16 +105,16 @@ func newConfigRmCmd(stack *string) *cobra.Command {
|
|||
return errors.Wrap(err, "invalid configuration key")
|
||||
}
|
||||
|
||||
var stackToSave tokens.QName
|
||||
if !all {
|
||||
stackToSave = s.Name()
|
||||
ps, err := workspace.DetectProjectStack(s.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if save {
|
||||
return deleteProjectConfiguration(stackToSave, key)
|
||||
if ps.Config != nil {
|
||||
delete(ps.Config, key)
|
||||
}
|
||||
|
||||
return deleteWorkspaceConfiguration(stackToSave, key)
|
||||
return workspace.SaveProjectStack(s.Name(), ps)
|
||||
}),
|
||||
}
|
||||
|
||||
|
@ -131,9 +129,7 @@ func newConfigRmCmd(stack *string) *cobra.Command {
|
|||
}
|
||||
|
||||
func newConfigSetCmd(stack *string) *cobra.Command {
|
||||
var all bool
|
||||
var plaintext bool
|
||||
var save bool
|
||||
var secret bool
|
||||
|
||||
setCmd := &cobra.Command{
|
||||
|
@ -145,13 +141,6 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
|||
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)
|
||||
|
@ -202,18 +191,20 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
|||
v = config.NewValue(value)
|
||||
}
|
||||
|
||||
// And now save it.
|
||||
var stackToSave tokens.QName
|
||||
if !all {
|
||||
stackToSave = s.Name()
|
||||
ps, err := workspace.DetectProjectStack(s.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setConfiguration(stackToSave, key, v, save)
|
||||
|
||||
ps.Config[key] = v
|
||||
|
||||
err = workspace.SaveProjectStack(s.Name(), ps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we saved a plaintext configuration value, and --plaintext was not passed, warn the user.
|
||||
if !secret && !plaintext && save {
|
||||
if !secret && !plaintext {
|
||||
cmdutil.Diag().Warningf(
|
||||
diag.Message(
|
||||
"saved config key '%s' value '%s' as plaintext; "+
|
||||
|
@ -225,15 +216,9 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
|||
}),
|
||||
}
|
||||
|
||||
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")
|
||||
|
@ -276,20 +261,14 @@ func prettyKeyForProject(key string, proj *workspace.Project) string {
|
|||
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())
|
||||
ps, err := workspace.DetectProjectStack(stack.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := ps.Config
|
||||
|
||||
// By default, we will use a blinding decrypter to show '******'. If requested, display secrets in plaintext.
|
||||
var decrypter config.Decrypter
|
||||
if cfg.HasSecureValue() && showSecrets {
|
||||
|
@ -301,167 +280,60 @@ func listConfig(stack backend.Stack, showSecrets bool) error {
|
|||
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)
|
||||
}
|
||||
// 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", "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)
|
||||
}
|
||||
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())
|
||||
ps, err := workspace.DetectProjectStack(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()
|
||||
cfg := ps.Config
|
||||
|
||||
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")
|
||||
}
|
||||
raw, err := v.Value(d)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not decrypt configuation value")
|
||||
}
|
||||
fmt.Printf("%v\n", raw)
|
||||
return nil
|
||||
} else {
|
||||
d = config.NewPanicCrypter()
|
||||
}
|
||||
raw, err := v.Value(d)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not decrypt configuration 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()
|
||||
}
|
||||
|
|
|
@ -31,12 +31,15 @@ func NewPulumiCmd() *cobra.Command {
|
|||
Use: "pulumi",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if cwd != "" {
|
||||
err := os.Chdir(cwd)
|
||||
if err != nil {
|
||||
if err := os.Chdir(cwd); err != nil {
|
||||
cmdutil.ExitError(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err := upgradeConfigurationFiles(); err != nil {
|
||||
cmdutil.ExitError(err.Error())
|
||||
}
|
||||
|
||||
cmdutil.InitLogging(logToStderr, verbose, logFlow)
|
||||
cmdutil.InitTracing("pulumi-cli", tracing)
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -12,6 +13,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/diag/colors"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
)
|
||||
|
||||
func newStackRmCmd() *cobra.Command {
|
||||
|
@ -53,11 +55,16 @@ func newStackRmCmd() *cobra.Command {
|
|||
return err
|
||||
}
|
||||
|
||||
err = deleteAllStackConfiguration(s.Name())
|
||||
// Blow away stack specific settings if they exist
|
||||
path, err := workspace.DetectProjectStackPath(s.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("%sStack '%s' has been removed!%s", colors.SpecAttention, s.Name(), colors.Reset)
|
||||
fmt.Println(colors.ColorizeText(msg))
|
||||
|
||||
|
|
191
cmd/upgrade_config.go
Normal file
191
cmd/upgrade_config.go
Normal file
|
@ -0,0 +1,191 @@
|
|||
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
)
|
||||
|
||||
// upgradeConfigurationFiles does an upgrade to move from the old configuration system (where we had config stored) in
|
||||
// workspace settings and Pulumi.yaml, to the new system where configuration data is stored in Pulumi.<stack-name>.yaml
|
||||
func upgradeConfigurationFiles() error {
|
||||
bes, _ := allBackends()
|
||||
|
||||
skippedUpgrade := false
|
||||
|
||||
for _, b := range bes {
|
||||
stacks, err := b.ListStacks()
|
||||
if err != nil {
|
||||
skippedUpgrade = true
|
||||
continue
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
stackName := stack.Name()
|
||||
|
||||
// If the new file exists, we can skip upgrading this stack.
|
||||
newFile, err := workspace.DetectProjectStackPath(stackName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "upgrade project")
|
||||
}
|
||||
|
||||
_, err = os.Stat(newFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrap(err, "upgrading project")
|
||||
} else if err == nil {
|
||||
// new file was present, skip upgrading this stack.
|
||||
continue
|
||||
}
|
||||
|
||||
cfg, salt, err := getOldConfiguration(stackName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "upgrading project")
|
||||
}
|
||||
|
||||
ps, err := workspace.DetectProjectStack(stackName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "upgrading project")
|
||||
}
|
||||
|
||||
ps.Config = cfg
|
||||
ps.EncryptionSalt = salt
|
||||
|
||||
if err := workspace.SaveProjectStack(stackName, ps); err != nil {
|
||||
return errors.Wrap(err, "upgrading project")
|
||||
}
|
||||
|
||||
if err := removeOldConfiguration(stackName); err != nil {
|
||||
return errors.Wrap(err, "upgrading project")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !skippedUpgrade {
|
||||
return removeOldProjectConfiguration()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getOldConfiguration reads the configuration for a given stack from the current workspace. It applies a hierarchy
|
||||
// of configuration settings based on stack overrides and workspace-wide global settings. If any of the workspace
|
||||
// settings had an impact on the values returned, the second return value will be true. It also returns the encryption
|
||||
// salt used for the stack.
|
||||
func getOldConfiguration(stackName tokens.QName) (config.Map, string, error) {
|
||||
contract.Require(stackName != "", "stackName")
|
||||
|
||||
// Get the workspace and package and get ready to merge their views of the configuration.
|
||||
ws, err := workspace.New()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
proj, err := workspace.DetectProject()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// We need to apply workspace and project configuration values in the right order. Basically, we want to
|
||||
// end up taking the most specific settings, where per-stack configuration is more specific than global, and
|
||||
// project configuration is more specific than workspace.
|
||||
result := make(config.Map)
|
||||
|
||||
// First, apply project-local stack-specific configuration.
|
||||
if stack, has := proj.StacksDeprecated[stackName]; has {
|
||||
for key, value := range stack.Config {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Now, apply workspace stack-specific configuration.
|
||||
if wsStackConfig, has := ws.Settings().ConfigDeprecated[stackName]; has {
|
||||
for key, value := range wsStackConfig {
|
||||
if _, has := result[key]; !has {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next, take anything from the global settings in our project file.
|
||||
for key, value := range proj.ConfigDeprecated {
|
||||
if _, has := result[key]; !has {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, take anything left in the workspace's global configuration.
|
||||
if wsGlobalConfig, has := ws.Settings().ConfigDeprecated[""]; has {
|
||||
for key, value := range wsGlobalConfig {
|
||||
if _, has := result[key]; !has {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now, get the encryption key. A stack specific one overrides the global one (global encryption keys were
|
||||
// deprecated previously)
|
||||
encryptionSalt := proj.EncryptionSaltDeprecated
|
||||
if stack, has := proj.StacksDeprecated[stackName]; has && stack.EncryptionSalt != "" {
|
||||
encryptionSalt = stack.EncryptionSalt
|
||||
}
|
||||
|
||||
return result, encryptionSalt, nil
|
||||
}
|
||||
|
||||
// removeOldConfiguration deletes all configuration information about a stack from both the workspace
|
||||
// and the project file. It does not touch the newly added Pulumi.<stack-name>.yaml file.
|
||||
func removeOldConfiguration(stackName tokens.QName) error {
|
||||
ws, err := workspace.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj, err := workspace.DetectProject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(proj.StacksDeprecated, stackName)
|
||||
delete(ws.Settings().ConfigDeprecated, stackName)
|
||||
|
||||
if err := ws.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := workspace.SaveProject(proj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeOldProjectConfiguration deletes all project level configuration information from both the workspace and the
|
||||
// project file.
|
||||
func removeOldProjectConfiguration() error {
|
||||
ws, err := workspace.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj, err := workspace.DetectProject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proj.EncryptionSaltDeprecated = ""
|
||||
proj.ConfigDeprecated = nil
|
||||
ws.Settings().ConfigDeprecated = nil
|
||||
|
||||
if err := ws.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := workspace.SaveProject(proj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -22,7 +22,6 @@ import (
|
|||
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/backend/state"
|
||||
"github.com/pulumi/pulumi/pkg/diag"
|
||||
"github.com/pulumi/pulumi/pkg/diag/colors"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
|
@ -565,12 +564,12 @@ func (b *cloudBackend) makeProgramUpdateRequest(stackName tokens.QName, proj *wo
|
|||
m backend.UpdateMetadata, opts engine.UpdateOptions) (apitype.UpdateProgramRequest, error) {
|
||||
|
||||
// Convert the configuration into its wire form.
|
||||
cfg, err := state.Configuration(b.d, stackName)
|
||||
stk, err := workspace.DetectProjectStack(stackName)
|
||||
if err != nil {
|
||||
return apitype.UpdateProgramRequest{}, errors.Wrap(err, "getting configuration")
|
||||
}
|
||||
wireConfig := make(map[string]apitype.ConfigValue)
|
||||
for k, cv := range cfg {
|
||||
for k, cv := range stk.Config {
|
||||
v, err := cv.Value(config.NopDecrypter)
|
||||
contract.AssertNoError(err)
|
||||
|
||||
|
|
|
@ -38,63 +38,30 @@ func defaultCrypter(stackName tokens.QName, cfg config.Map) (config.Crypter, err
|
|||
|
||||
// symmetricCrypter gets the right value encrypter/decrypter for this project.
|
||||
func symmetricCrypter(stackName tokens.QName) (config.Crypter, error) {
|
||||
// First, read the package to see if we've got a key.
|
||||
proj, err := workspace.DetectProject()
|
||||
contract.Assertf(stackName != "", "stackName", "!= \"\"")
|
||||
|
||||
info, err := workspace.DetectProjectStack(stackName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if proj.Stacks == nil {
|
||||
proj.Stacks = make(map[tokens.QName]workspace.ProjectStack)
|
||||
// If we have a salt, we can just use it.
|
||||
if info.EncryptionSalt != "" {
|
||||
phrase, phraseErr := readPassphrase("Enter your passphrase to unlock config/secrets\n" +
|
||||
" (set PULUMI_CONFIG_PASSPHRASE to remember)")
|
||||
if phraseErr != nil {
|
||||
return nil, phraseErr
|
||||
}
|
||||
|
||||
crypter, crypterErr := symmetricCrypterFromPhraseAndState(phrase, info.EncryptionSalt)
|
||||
if crypterErr != nil {
|
||||
return nil, crypterErr
|
||||
}
|
||||
|
||||
return crypter, nil
|
||||
}
|
||||
|
||||
// If we have a top level EncryptionSalt, we are reading an older version of Pulumi.yaml where local stacks shared
|
||||
// a key. To migrate, we'll simply move this salt to any local stack that has encrypted config and then unset the
|
||||
// package wide salt.
|
||||
if proj.EncryptionSalt != "" {
|
||||
localStacks, stacksErr := getLocalStacks()
|
||||
if stacksErr != nil {
|
||||
return nil, stacksErr
|
||||
}
|
||||
|
||||
for _, localStack := range localStacks {
|
||||
stackInfo := proj.Stacks[localStack]
|
||||
contract.Assertf(stackInfo.EncryptionSalt == "", "package and stack %v had an encryption salt", localStack)
|
||||
|
||||
if stackInfo.Config.HasSecureValue() {
|
||||
stackInfo.EncryptionSalt = proj.EncryptionSalt
|
||||
}
|
||||
|
||||
proj.Stacks[localStack] = stackInfo
|
||||
}
|
||||
|
||||
proj.EncryptionSalt = ""
|
||||
|
||||
// Now store the result on the package and save it.
|
||||
if err = workspace.SaveProject(proj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If there's already a salt for the local stack, we can just use that.
|
||||
if info, has := proj.Stacks[stackName]; has {
|
||||
if info.EncryptionSalt != "" {
|
||||
phrase, phraseErr := readPassphrase("Enter your passphrase to unlock config/secrets\n" +
|
||||
" (set PULUMI_CONFIG_PASSPHRASE to remember)")
|
||||
if phraseErr != nil {
|
||||
return nil, phraseErr
|
||||
}
|
||||
|
||||
crypter, crypterErr := symmetricCrypterFromPhraseAndState(phrase, info.EncryptionSalt)
|
||||
if crypterErr != nil {
|
||||
return nil, crypterErr
|
||||
}
|
||||
|
||||
return crypter, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Read a passphrase and confirm it.
|
||||
// Here, the stack does not have an EncryptionSalt, so we will get a passphrase and create one
|
||||
phrase, err := readPassphrase("Enter your passphrase to protect config/secrets")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -117,11 +84,9 @@ func symmetricCrypter(stackName tokens.QName) (config.Crypter, error) {
|
|||
msg, err := crypter.EncryptValue("pulumi")
|
||||
contract.AssertNoError(err)
|
||||
|
||||
// Now store the result on the package and save it.
|
||||
stackInfo := proj.Stacks[stackName]
|
||||
stackInfo.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
|
||||
proj.Stacks[stackName] = stackInfo
|
||||
if err = workspace.SaveProject(proj); err != nil {
|
||||
// Now store the result and save it.
|
||||
info.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
|
||||
if err = workspace.SaveProjectStack(stackName, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/backend/state"
|
||||
"github.com/pulumi/pulumi/pkg/encoding"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
|
@ -94,11 +93,11 @@ func (b *localBackend) newUpdate(stackName tokens.QName, proj *workspace.Project
|
|||
}
|
||||
|
||||
func (b *localBackend) getTarget(stackName tokens.QName) (*deploy.Target, error) {
|
||||
cfg, err := state.Configuration(b.d, stackName)
|
||||
stk, err := workspace.DetectProjectStack(stackName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypter, err := defaultCrypter(stackName, cfg)
|
||||
decrypter, err := defaultCrypter(stackName, stk.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -108,7 +107,7 @@ func (b *localBackend) getTarget(stackName tokens.QName) (*deploy.Target, error)
|
|||
}
|
||||
return &deploy.Target{
|
||||
Name: stackName,
|
||||
Config: cfg,
|
||||
Config: stk.Config,
|
||||
Decrypter: decrypter,
|
||||
Snapshot: snapshot,
|
||||
}, nil
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||
|
||||
package state
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"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/contract"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
)
|
||||
|
||||
// Configuration reads the configuration for a given stack from the current workspace. It applies a hierarchy of
|
||||
// configuration settings based on stack overrides and workspace-wide global settings. If any of the workspace
|
||||
// settings had an impact on the values returned, the second return value will be true.
|
||||
func Configuration(d diag.Sink, stackName tokens.QName) (config.Map, error) {
|
||||
contract.Require(stackName != "", "stackName")
|
||||
|
||||
// Get the workspace and package and get ready to merge their views of the configuration.
|
||||
ws, err := workspace.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proj, err := workspace.DetectProject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to apply workspace and project configuration values in the right order. Basically, we want to
|
||||
// end up taking the most specific settings, where per-stack configuration is more specific than global, and
|
||||
// project configuration is more specific than workspace.
|
||||
result := make(config.Map)
|
||||
var workspaceConfigKeys []string
|
||||
|
||||
// First, apply project-local stack-specific configuration.
|
||||
if stack, has := proj.Stacks[stackName]; has {
|
||||
for key, value := range stack.Config {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Now, apply workspace stack-specific configuration.
|
||||
if wsStackConfig, has := ws.Settings().Config[stackName]; has {
|
||||
for key, value := range wsStackConfig {
|
||||
if _, has := result[key]; !has {
|
||||
result[key] = value
|
||||
workspaceConfigKeys = append(workspaceConfigKeys, string(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next, take anything from the global settings in our project file.
|
||||
for key, value := range proj.Config {
|
||||
if _, has := result[key]; !has {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, take anything left in the workspace's global configuration.
|
||||
if wsGlobalConfig, has := ws.Settings().Config[""]; has {
|
||||
for key, value := range wsGlobalConfig {
|
||||
if _, has := result[key]; !has {
|
||||
result[key] = value
|
||||
workspaceConfigKeys = append(workspaceConfigKeys, string(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are any configuration settings from the workspace being used, issue a warning. This can be a subtle
|
||||
// source of discrepancy when deploying stacks to the cloud, and can be tough to track down.
|
||||
if len(workspaceConfigKeys) > 0 {
|
||||
sort.Strings(workspaceConfigKeys)
|
||||
d.Warningf(
|
||||
diag.Message("configuration variables were taken from your local workspace; proceed with caution: %v"),
|
||||
workspaceConfigKeys)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
package workspace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/encoding"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/util/fsutil"
|
||||
)
|
||||
|
||||
|
@ -44,9 +46,20 @@ func DetectProjectPath() (string, error) {
|
|||
return path, nil
|
||||
}
|
||||
|
||||
// DetectProjectStackPath returns the name of the file to store stack specific project settings in. We place stack
|
||||
// specific settings next to the Pulumi.yaml file, named like: Pulumi.<stack-name>.yaml
|
||||
func DetectProjectStackPath(stackName tokens.QName) (string, error) {
|
||||
projPath, err := DetectProjectPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(filepath.Dir(projPath), fmt.Sprintf("%s.%s%s", ProjectFile, qnameFileName(stackName),
|
||||
filepath.Ext(projPath))), nil
|
||||
}
|
||||
|
||||
// DetectProjectPathFrom locates the closest project from the given path, searching "upwards" in the directory
|
||||
// hierarchy. If no project is found, an empty path is returned. If problems are detected, they are logged to
|
||||
// the diag.Sink.
|
||||
// hierarchy. If no project is found, an empty path is returned.
|
||||
func DetectProjectPathFrom(path string) (string, error) {
|
||||
return fsutil.WalkUp(path, isProject, func(s string) bool {
|
||||
return !isRepositoryFolder(filepath.Join(s, BookkeepingDir))
|
||||
|
@ -59,6 +72,15 @@ func DetectProject() (*Project, error) {
|
|||
return proj, err
|
||||
}
|
||||
|
||||
func DetectProjectStack(stackName tokens.QName) (*ProjectStack, error) {
|
||||
path, err := DetectProjectStackPath(stackName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return LoadProjectStack(path)
|
||||
}
|
||||
|
||||
// DetectProjectAndPath loads the closest package from the current working directory, or an error if not found. It
|
||||
// also returns the path where the package was found.
|
||||
func DetectProjectAndPath() (*Project, string, error) {
|
||||
|
@ -83,9 +105,18 @@ func SaveProject(proj *Project) error {
|
|||
return proj.Save(path)
|
||||
}
|
||||
|
||||
func SaveProjectStack(stackName tokens.QName, stack *ProjectStack) error {
|
||||
path, err := DetectProjectStackPath(stackName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stack.Save(path)
|
||||
}
|
||||
|
||||
func isGitFolder(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir() && info.Name() == ".git"
|
||||
return err == nil && info.IsDir() && info.Name() == GitDir
|
||||
}
|
||||
|
||||
func isRepositoryFolder(path string) bool {
|
||||
|
|
|
@ -4,6 +4,7 @@ package workspace
|
|||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
|
@ -38,13 +39,13 @@ type Project struct {
|
|||
|
||||
Analyzers *Analyzers `json:"analyzers,omitempty" yaml:"analyzers,omitempty"` // any analyzers enabled for this project.
|
||||
|
||||
EncryptionSalt string `json:"encryptionsalt,omitempty" yaml:"encryptionsalt,omitempty"` // base64 encoded encryption salt.
|
||||
Context string `json:"context,omitempty" yaml:"context,omitempty"` // an optional path (combined with the on disk location of Pulumi.yaml) to control the data uploaded to the service.
|
||||
NoDefaultIgnores *bool `json:"nodefaultignores,omitempty" yaml:"nodefaultignores,omitempty"` // true if we should only respect .pulumiignore when archiving
|
||||
EncryptionSaltDeprecated string `json:"encryptionsalt,omitempty" yaml:"encryptionsalt,omitempty"` // base64 encoded encryption salt.
|
||||
Context string `json:"context,omitempty" yaml:"context,omitempty"` // an optional path (combined with the on disk location of Pulumi.yaml) to control the data uploaded to the service.
|
||||
NoDefaultIgnores *bool `json:"nodefaultignores,omitempty" yaml:"nodefaultignores,omitempty"` // true if we should only respect .pulumiignore when archiving
|
||||
|
||||
Config map[tokens.ModuleMember]config.Value `json:"config,omitempty" yaml:"config,omitempty"` // optional config (applies to all stacks).
|
||||
ConfigDeprecated map[tokens.ModuleMember]config.Value `json:"config,omitempty" yaml:"config,omitempty"` // optional config (applies to all stacks).
|
||||
|
||||
Stacks map[tokens.QName]ProjectStack `json:"stacks,omitempty" yaml:"stacks,omitempty"` // optional stack specific information.
|
||||
StacksDeprecated map[tokens.QName]ProjectStack `json:"stacks,omitempty" yaml:"stacks,omitempty"` // optional stack specific information.
|
||||
}
|
||||
|
||||
func (proj *Project) Validate() error {
|
||||
|
@ -65,15 +66,15 @@ func (proj *Project) UseDefaultIgnores() bool {
|
|||
return !(*proj.NoDefaultIgnores)
|
||||
}
|
||||
|
||||
// Save writes a project defitiniton to a file.
|
||||
// Save writes a project definition to a file.
|
||||
func (proj *Project) Save(path string) error {
|
||||
contract.Require(path != "", "path")
|
||||
contract.Require(proj != nil, "proj")
|
||||
contract.Requiref(proj.Validate() == nil, "proj", "Validate()")
|
||||
|
||||
for name, info := range proj.Stacks {
|
||||
if info.IsEmpty() {
|
||||
delete(proj.Stacks, name)
|
||||
for name, info := range proj.StacksDeprecated {
|
||||
if info.isEmpty() {
|
||||
delete(proj.StacksDeprecated, name)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,14 +98,32 @@ type ProjectStack struct {
|
|||
Config config.Map `json:"config,omitempty" yaml:"config,omitempty"` // optional config.
|
||||
}
|
||||
|
||||
// IsEmpty returns True if this object contains no information (i.e. all members have their zero values)
|
||||
func (s *ProjectStack) IsEmpty() bool {
|
||||
return len(s.Config) == 0 && s.EncryptionSalt == ""
|
||||
// isEmpty returns True if this object contains no information (i.e. all members have their zero values)
|
||||
func (ps *ProjectStack) isEmpty() bool {
|
||||
return len(ps.Config) == 0 && ps.EncryptionSalt == ""
|
||||
}
|
||||
|
||||
// Save writes a project definition to a file.
|
||||
func (ps *ProjectStack) Save(path string) error {
|
||||
contract.Require(path != "", "path")
|
||||
contract.Require(ps != nil, "ps")
|
||||
|
||||
m, err := marshallerForPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := m.Marshal(ps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, b, 0644)
|
||||
}
|
||||
|
||||
// LoadProject reads a project definition from a file.
|
||||
func LoadProject(path string) (*Project, error) {
|
||||
contract.Require(path != "", "proj")
|
||||
contract.Require(path != "", "path")
|
||||
|
||||
m, err := marshallerForPath(path)
|
||||
if err != nil {
|
||||
|
@ -130,6 +149,37 @@ func LoadProject(path string) (*Project, error) {
|
|||
return &proj, err
|
||||
}
|
||||
|
||||
// LoadProjectStack reads a stack definition from a file.
|
||||
func LoadProjectStack(path string) (*ProjectStack, error) {
|
||||
contract.Require(path != "", "path")
|
||||
|
||||
m, err := marshallerForPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
return &ProjectStack{
|
||||
Config: make(config.Map),
|
||||
}, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ps ProjectStack
|
||||
err = m.Unmarshal(b, &ps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ps.Config == nil {
|
||||
ps.Config = make(config.Map)
|
||||
}
|
||||
|
||||
return &ps, err
|
||||
}
|
||||
|
||||
func marshallerForPath(path string) (encoding.Marshaler, error) {
|
||||
ext := filepath.Ext(path)
|
||||
m, has := encoding.Marshalers[ext]
|
||||
|
|
|
@ -10,6 +10,6 @@ import (
|
|||
// Settings defines workspace settings shared amongst many related projects.
|
||||
// nolint: lll
|
||||
type Settings struct {
|
||||
Stack tokens.QName `json:"stack,omitempty" yaml:"env,omitempty"` // an optional default stack to use.
|
||||
Config map[tokens.QName]config.Map `json:"config,omitempty" yaml:"config,omitempty"` // optional workspace local configuration (overrides values in a project)
|
||||
Stack tokens.QName `json:"stack,omitempty" yaml:"env,omitempty"` // an optional default stack to use.
|
||||
ConfigDeprecated map[tokens.QName]config.Map `json:"config,omitempty" yaml:"config,omitempty"` // optional workspace local configuration (overrides values in a project)
|
||||
}
|
||||
|
|
|
@ -25,12 +25,11 @@ type W interface {
|
|||
StackPath(stack tokens.QName) string // returns the path to store stack information.
|
||||
BackupDirectory() (string, error) // returns the directory to store backup stack files.
|
||||
HistoryDirectory(stack tokens.QName) string // returns the directory to store a stack's history information.
|
||||
Project() (*Project, error) // returns a copy of the project associated with this workspace.
|
||||
Save() error // saves any modifications to the workspace.
|
||||
}
|
||||
|
||||
type projectWorkspace struct {
|
||||
name tokens.PackageName // the project this workspace is associated with.
|
||||
name tokens.PackageName // the package this workspace is associated with.
|
||||
project string // the path to the Pulumi.[yaml|json] file for this project.
|
||||
settings *Settings // settings for this workspace.
|
||||
repo *Repository // the repo this workspace is associated with.
|
||||
|
@ -76,8 +75,8 @@ func NewFrom(dir string) (W, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if w.settings.Config == nil {
|
||||
w.settings.Config = make(map[tokens.QName]config.Map)
|
||||
if w.settings.ConfigDeprecated == nil {
|
||||
w.settings.ConfigDeprecated = make(map[tokens.QName]config.Map)
|
||||
}
|
||||
|
||||
return &w, nil
|
||||
|
@ -91,15 +90,11 @@ func (pw *projectWorkspace) Repository() *Repository {
|
|||
return pw.repo
|
||||
}
|
||||
|
||||
func (pw *projectWorkspace) Project() (*Project, error) {
|
||||
return LoadProject(pw.project)
|
||||
}
|
||||
|
||||
func (pw *projectWorkspace) Save() error {
|
||||
// let's remove all the empty entries from the config array
|
||||
for k, v := range pw.settings.Config {
|
||||
for k, v := range pw.settings.ConfigDeprecated {
|
||||
if len(v) == 0 {
|
||||
delete(pw.settings.Config, k)
|
||||
delete(pw.settings.ConfigDeprecated, k)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,6 +177,11 @@ func sha1HexString(value string) string {
|
|||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// qnameFileName takes a qname and cleans it for use as a filename (by replacing tokens.QNameDelimter with a dash)
|
||||
func qnameFileName(nm tokens.QName) string {
|
||||
return strings.Replace(string(nm), tokens.QNameDelimiter, "-", -1)
|
||||
}
|
||||
|
||||
// qnamePath just cleans a name and makes sure it's appropriate to use as a path.
|
||||
func qnamePath(nm tokens.QName) string {
|
||||
return stringNamePath(string(nm))
|
||||
|
|
3
tests/integration/config_upgrade/.gitignore
vendored
Normal file
3
tests/integration/config_upgrade/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/bin/
|
||||
/node_modules/
|
||||
!.pulumi/
|
4
tests/integration/config_upgrade/.pulumi/settings.json
Normal file
4
tests/integration/config_upgrade/.pulumi/settings.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"owner": "pulumi",
|
||||
"name": "config_upgrade"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"stack": "local1"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"stack": "local2"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"stack": "local2",
|
||||
"config": {
|
||||
"": {
|
||||
"config_upgrade:config:allWorkspaceKey": "allWorkspaceValue"
|
||||
},
|
||||
"local2": {
|
||||
"config_upgrade:config:allKeyOverride": "local2Overridden",
|
||||
"config_upgrade:config:local2WorkspaceKey": "local2WorkspaceValue"
|
||||
}
|
||||
}
|
||||
}
|
15
tests/integration/config_upgrade/Pulumi.yaml
Normal file
15
tests/integration/config_upgrade/Pulumi.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: config_upgrade
|
||||
runtime: nodejs
|
||||
description: A program with configuration using the old system, to upgrade
|
||||
config:
|
||||
config_upgrade:config:allKey: allValue
|
||||
stacks:
|
||||
local1:
|
||||
config:
|
||||
config_upgrade:config:allKeyOverride: local1Overridden
|
||||
config_upgrade:config:local1ProjectKey: local1ProjectValue
|
||||
local2:
|
||||
encryptionsalt: v1:IgONFZwgKOc=:v1:r8xlbKZUVBmPec1V:tDMSu68h0nsCdMaIpSyqsmg9xju+gw==
|
||||
config:
|
||||
config_upgrade:config:savedSecret:
|
||||
secure: v1:+xA1n2mraqtcyg4S:9FckqDz0MF8TyQCX+edpmldxZQ==
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
ptesting "github.com/pulumi/pulumi/pkg/testing"
|
||||
"github.com/pulumi/pulumi/pkg/testing/integration"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
|
@ -217,80 +218,107 @@ func TestConfigSave(t *testing.T) {
|
|||
e.RunCommand("pulumi", "stack", "init", "--local", "testing-1")
|
||||
|
||||
// Now configure and save a few different things:
|
||||
// 1) do not save.
|
||||
e.RunCommand("pulumi", "config", "set", "configA", "value1", "--save=false")
|
||||
// 2) save to the project file, under the current stack.
|
||||
e.RunCommand("pulumi", "config", "set", "configB", "value2")
|
||||
// 3) save to the project file, underneath an entirely different stack.
|
||||
e.RunCommand("pulumi", "config", "set", "configC", "value3", "--stack", "testing-2")
|
||||
// 4) save to the project file, across all stacks.
|
||||
e.RunCommand("pulumi", "config", "set", "configD", "value4", "--all")
|
||||
// 5) save the same config key with a different value in the stack versus all stacks.
|
||||
e.RunCommand("pulumi", "config", "set", "configE", "value55")
|
||||
e.RunCommand("pulumi", "config", "set", "configE", "value66", "--all")
|
||||
e.RunCommand("pulumi", "config", "set", "configA", "value1")
|
||||
e.RunCommand("pulumi", "config", "set", "configB", "value2", "--stack", "testing-2")
|
||||
|
||||
e.RunCommand("pulumi", "stack", "select", "testing-2")
|
||||
|
||||
e.RunCommand("pulumi", "config", "set", "configD", "value4")
|
||||
e.RunCommand("pulumi", "config", "set", "configC", "value3", "--stack", "testing-1")
|
||||
|
||||
// Now read back the config using the CLI:
|
||||
{
|
||||
stdout, _ := e.RunCommand("pulumi", "config", "get", "configA")
|
||||
assert.Equal(t, "value1\n", stdout)
|
||||
}
|
||||
{
|
||||
stdout, _ := e.RunCommand("pulumi", "config", "get", "configB")
|
||||
assert.Equal(t, "value2\n", stdout)
|
||||
}
|
||||
{
|
||||
// config is in a different stack, should yield a stderr:
|
||||
stdout, stderr := e.RunCommandExpectError("pulumi", "config", "get", "configC")
|
||||
// the config in a different stack, so this should error.
|
||||
stdout, stderr := e.RunCommandExpectError("pulumi", "config", "get", "configA")
|
||||
assert.Equal(t, "", stdout)
|
||||
assert.NotEqual(t, "", stderr)
|
||||
}
|
||||
{
|
||||
stdout, _ := e.RunCommand("pulumi", "config", "get", "configC", "--stack", "testing-2")
|
||||
assert.Equal(t, "value3\n", stdout)
|
||||
}
|
||||
{
|
||||
stdout, _ := e.RunCommand("pulumi", "config", "get", "configD")
|
||||
assert.Equal(t, "value4\n", stdout)
|
||||
}
|
||||
{
|
||||
stdout, _ := e.RunCommand("pulumi", "config", "get", "configE")
|
||||
assert.Equal(t, "value55\n", stdout)
|
||||
// but selecting the stack should let you see it
|
||||
stdout, _ := e.RunCommand("pulumi", "config", "get", "configA", "--stack", "testing-1")
|
||||
assert.Equal(t, "value1\n", stdout)
|
||||
}
|
||||
|
||||
// Finally, check that the project file contains what we expected.
|
||||
cfgkey := func(k string) tokens.ModuleMember { return tokens.ModuleMember("testing-config:config:" + k) }
|
||||
proj, err := workspace.LoadProject(path)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(proj.Config)) // --all
|
||||
d, ok := proj.Config[cfgkey("configD")]
|
||||
assert.True(t, ok)
|
||||
dv, err := d.Value(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value4", dv)
|
||||
ee, ok := proj.Config[cfgkey("configE")]
|
||||
assert.True(t, ok)
|
||||
ev, err := ee.Value(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value66", ev)
|
||||
assert.Equal(t, 2, len(proj.Stacks))
|
||||
assert.Equal(t, 2, len(proj.Stacks["testing-1"].Config))
|
||||
b, ok := proj.Stacks["testing-1"].Config[cfgkey("configB")]
|
||||
assert.True(t, ok)
|
||||
bv, err := b.Value(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value2", bv)
|
||||
e2, ok := proj.Stacks["testing-1"].Config[cfgkey("configE")]
|
||||
assert.True(t, ok)
|
||||
e2v, err := e2.Value(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value55", e2v)
|
||||
assert.Equal(t, 1, len(proj.Stacks["testing-2"].Config))
|
||||
c, ok := proj.Stacks["testing-2"].Config[cfgkey("configC")]
|
||||
assert.True(t, ok)
|
||||
cv, err := c.Value(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value3", cv)
|
||||
// Finally, check that the stack file contains what we expected.
|
||||
validate := func(k string, v string, cfg config.Map) {
|
||||
key := tokens.ModuleMember("testing-config:config:" + k)
|
||||
d, ok := cfg[key]
|
||||
assert.True(t, ok, "config key %v should be set", k)
|
||||
dv, err := d.Value(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v, dv)
|
||||
}
|
||||
|
||||
// We do not allow storing secrets for all stacks, since the encryption key for the secret is tied to the stack
|
||||
e.RunCommandExpectError("pulumi", "config", "set", "secretA", "valueA", "--all", "--secret")
|
||||
testStack1, err := workspace.LoadProjectStack(filepath.Join(e.CWD, "Pulumi.testing-1.yaml"))
|
||||
assert.NoError(t, err)
|
||||
testStack2, err := workspace.LoadProjectStack(filepath.Join(e.CWD, "Pulumi.testing-2.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(testStack1.Config))
|
||||
assert.Equal(t, 2, len(testStack2.Config))
|
||||
|
||||
validate("configA", "value1", testStack1.Config)
|
||||
validate("configC", "value3", testStack1.Config)
|
||||
|
||||
validate("configB", "value2", testStack2.Config)
|
||||
validate("configD", "value4", testStack2.Config)
|
||||
}
|
||||
|
||||
// Tests that when `pulumi` is run, configuration is upgraded from the old format to the new format.
|
||||
func TestConfigUpgrade(t *testing.T) {
|
||||
e := ptesting.NewEnvironment(t)
|
||||
defer func() {
|
||||
if !t.Failed() {
|
||||
e.DeleteEnvironment()
|
||||
}
|
||||
}()
|
||||
|
||||
e.ImportDirectory("config_upgrade")
|
||||
|
||||
// Run a pulumi command, which will upgrade everything.
|
||||
e.RunCommand("pulumi", "config")
|
||||
|
||||
validate := func(k string, v string, cfg config.Map) {
|
||||
key := tokens.ModuleMember("config_upgrade:config:" + k)
|
||||
d, ok := cfg[key]
|
||||
assert.True(t, ok, "config key %v should be set", k)
|
||||
dv, err := d.Value(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v, dv)
|
||||
}
|
||||
|
||||
testStack1, err := workspace.LoadProjectStack(filepath.Join(e.CWD, "Pulumi.local1.yaml"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 4, len(testStack1.Config))
|
||||
testStack2, err := workspace.LoadProjectStack(filepath.Join(e.CWD, "Pulumi.local2.yaml"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, len(testStack2.Config))
|
||||
|
||||
validate("allKey", "allValue", testStack1.Config)
|
||||
validate("allKeyOverride", "local1Overridden", testStack1.Config)
|
||||
validate("allWorkspaceKey", "allWorkspaceValue", testStack1.Config)
|
||||
validate("local1ProjectKey", "local1ProjectValue", testStack1.Config)
|
||||
|
||||
validate("allKey", "allValue", testStack2.Config)
|
||||
validate("allKeyOverride", "local2Overridden", testStack2.Config)
|
||||
validate("allWorkspaceKey", "allWorkspaceValue", testStack2.Config)
|
||||
validate("local2WorkspaceKey", "local2WorkspaceValue", testStack2.Config)
|
||||
|
||||
// The stack local2 had an encrypted configuration value, ensure the EncryptionSalt was copied over
|
||||
assert.NotEmpty(t, testStack2.EncryptionSalt)
|
||||
|
||||
// Ensure config has been removed from the old files:
|
||||
w, err := workspace.NewFrom(e.CWD)
|
||||
assert.NoError(t, err)
|
||||
|
||||
proj, err := workspace.LoadProject(filepath.Join(e.CWD, "Pulumi.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Empty(t, w.Settings().ConfigDeprecated)
|
||||
assert.Empty(t, proj.ConfigDeprecated)
|
||||
assert.Empty(t, proj.EncryptionSaltDeprecated)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue