Merge pull request #986 from pulumi/config-refactor

Rework config storage
This commit is contained in:
Matt Ellis 2018-03-02 13:46:49 -08:00 committed by GitHub
commit 96d7f9307a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 524 additions and 420 deletions

View file

@ -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()
}

View file

@ -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)
},

View file

@ -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
View 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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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 {

View file

@ -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]

View file

@ -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)
}

View file

@ -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))

View file

@ -0,0 +1,3 @@
/bin/
/node_modules/
!.pulumi/

View file

@ -0,0 +1,4 @@
{
"owner": "pulumi",
"name": "config_upgrade"
}

View file

@ -0,0 +1,3 @@
{
"stack": "local1"
}

View file

@ -0,0 +1,3 @@
{
"stack": "local2"
}

View file

@ -0,0 +1,12 @@
{
"stack": "local2",
"config": {
"": {
"config_upgrade:config:allWorkspaceKey": "allWorkspaceValue"
},
"local2": {
"config_upgrade:config:allKeyOverride": "local2Overridden",
"config_upgrade:config:local2WorkspaceKey": "local2WorkspaceValue"
}
}
}

View 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==

View file

@ -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)
}