Add a --config-file option for stack ops (#2258)

This option allows the user to override the file used to fetch and store
configuration information for a stack. It is available for the config,
destroy, logs, preview, refresh, and up commands.

Note that this option is not persistent: if it is not specified, the
stack's default configuration will be used. If an alternate config file
is used exclusively for a stack, it must be specified to all commands
that interact with that stack.

This option can be used to share plaintext configuration across multiple
stacks. It cannot be used to share secret configuration, as secrets are
associated with a particular stack and cannot be decryptex by other
stacks.
This commit is contained in:
Pat Gavlin 2018-11-30 15:11:05 -08:00 committed by GitHub
parent 55bea65276
commit 9c5526e7dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 134 additions and 53 deletions

View file

@ -1,3 +1,9 @@
## 0.16.7 (not yet released)
### Improvements
- Configuration and stack commands now take a `--config-file` options. This option allows the user to override the file used to fetch and store config information for a stack during the execution of a command.
## 0.16.6 (Released November 28th, 2018)

View file

@ -67,6 +67,9 @@ func newConfigCmd() *cobra.Command {
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
cmd.PersistentFlags().StringVar(
&stackConfigFile, "config-file", "",
"Use the configuration values in the specified file rather than detecting the file name")
cmd.AddCommand(newConfigGetCmd(&stack))
cmd.AddCommand(newConfigRmCmd(&stack))
@ -117,14 +120,13 @@ func newConfigRmCmd(stack *string) *cobra.Command {
if err != nil {
return err
}
stackName := s.Ref().Name()
key, err := parseConfigKey(args[0])
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
ps, err := workspace.DetectProjectStack(stackName)
ps, err := loadProjectStack(s)
if err != nil {
return err
}
@ -133,7 +135,7 @@ func newConfigRmCmd(stack *string) *cobra.Command {
delete(ps.Config, key)
}
return workspace.SaveProjectStack(stackName, ps)
return saveProjectStack(s, ps)
}),
}
@ -156,14 +158,13 @@ func newConfigRefreshCmd(stack *string) *cobra.Command {
if err != nil {
return err
}
stackName := s.Ref().Name()
c, err := backend.GetLatestConfiguration(commandContext(), s)
if err != nil {
return err
}
configPath, err := workspace.DetectProjectStackPath(stackName)
configPath, err := getProjectStackPath(s)
if err != nil {
return err
}
@ -201,7 +202,7 @@ func newConfigRefreshCmd(stack *string) *cobra.Command {
err = ps.Save(configPath)
if err == nil {
fmt.Printf("refreshed configuration for stack '%s'\n", stackName)
fmt.Printf("refreshed configuration for stack '%s'\n", s.Ref().Name())
}
return err
}),
@ -233,7 +234,6 @@ func newConfigSetCmd(stack *string) *cobra.Command {
if err != nil {
return err
}
stackName := s.Ref().Name()
key, err := parseConfigKey(args[0])
if err != nil {
@ -286,14 +286,14 @@ func newConfigSetCmd(stack *string) *cobra.Command {
}
}
ps, err := workspace.DetectProjectStack(stackName)
ps, err := loadProjectStack(s)
if err != nil {
return err
}
ps.Config[key] = v
return workspace.SaveProjectStack(stackName, ps)
return saveProjectStack(s, ps)
}),
}
@ -307,6 +307,29 @@ func newConfigSetCmd(stack *string) *cobra.Command {
return setCmd
}
var stackConfigFile string
func getProjectStackPath(stack backend.Stack) (string, error) {
if stackConfigFile == "" {
return workspace.DetectProjectStackPath(stack.Ref().Name())
}
return stackConfigFile, nil
}
func loadProjectStack(stack backend.Stack) (*workspace.ProjectStack, error) {
if stackConfigFile == "" {
return workspace.DetectProjectStack(stack.Ref().Name())
}
return workspace.LoadProjectStack(stackConfigFile)
}
func saveProjectStack(stack backend.Stack, ps *workspace.ProjectStack) error {
if stackConfigFile == "" {
return workspace.SaveProjectStack(stack.Ref().Name(), ps)
}
return ps.Save(stackConfigFile)
}
func parseConfigKey(key string) (config.Key, error) {
// As a convience, we'll treat any key with no delimiter as if:
// <program-name>:<key> had been written instead
@ -340,7 +363,7 @@ func prettyKeyForProject(k config.Key, proj *workspace.Project) string {
}
func listConfig(stack backend.Stack, showSecrets bool) error {
ps, err := workspace.DetectProjectStack(stack.Ref().Name())
ps, err := loadProjectStack(stack)
if err != nil {
return err
}
@ -391,7 +414,7 @@ func listConfig(stack backend.Stack, showSecrets bool) error {
}
func getConfig(stack backend.Stack, key config.Key) error {
ps, err := workspace.DetectProjectStack(stack.Ref().Name())
ps, err := loadProjectStack(stack)
if err != nil {
return err
}

View file

@ -120,6 +120,9 @@ func newDestroyCmd() *cobra.Command {
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
cmd.PersistentFlags().StringVar(
&stackConfigFile, "config-file", "",
"Use the configuration values in the specified file rather than detecting the file name")
cmd.PersistentFlags().StringVarP(
&message, "message", "m", "",
"Optional message to associate with the destroy operation")

View file

@ -87,9 +87,9 @@ func newLoginCmd() *cobra.Command {
var be backend.Backend
var err error
if filestate.IsLocalBackendURL(cloudURL) {
be, err = filestate.Login(cmdutil.Diag(), cloudURL)
be, err = filestate.Login(cmdutil.Diag(), cloudURL, "")
} else {
be, err = httpstate.Login(commandContext(), cmdutil.Diag(), cloudURL, displayOptions)
be, err = httpstate.Login(commandContext(), cmdutil.Diag(), cloudURL, "", displayOptions)
}
if err != nil {
return errors.Wrapf(err, "problem logging in")

View file

@ -69,9 +69,9 @@ func newLogoutCmd() *cobra.Command {
var be backend.Backend
var err error
if filestate.IsLocalBackendURL(cloudURL) {
be, err = filestate.New(cmdutil.Diag(), cloudURL)
be, err = filestate.New(cmdutil.Diag(), cloudURL, "")
} else {
be, err = httpstate.New(cmdutil.Diag(), cloudURL)
be, err = httpstate.New(cmdutil.Diag(), cloudURL, "")
}
if err != nil {
return err

View file

@ -144,6 +144,9 @@ func newLogsCmd() *cobra.Command {
logsCmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
logsCmd.PersistentFlags().StringVar(
&stackConfigFile, "config-file", "",
"Use the configuration values in the specified file rather than detecting the file name")
logsCmd.PersistentFlags().BoolVarP(
&jsonOut, "json", "j", false, "Emit outputs as JSON")
logsCmd.PersistentFlags().BoolVarP(

View file

@ -443,8 +443,8 @@ func stackInit(b backend.Backend, stackName string, setCurrent bool) (backend.St
}
// saveConfig saves the config for the stack.
func saveConfig(stackName tokens.QName, c config.Map) error {
ps, err := workspace.DetectProjectStack(stackName)
func saveConfig(stack backend.Stack, c config.Map) error {
ps, err := loadProjectStack(stack)
if err != nil {
return err
}
@ -453,7 +453,7 @@ func saveConfig(stackName tokens.QName, c config.Map) error {
ps.Config[k] = v
}
return workspace.SaveProjectStack(stackName, ps)
return saveProjectStack(stack, ps)
}
// installDependencies will install dependencies for the project, e.g. by running

View file

@ -96,7 +96,7 @@ func newPluginInstallCmd() *cobra.Command {
// Target the cloud URL for downloads.
var releases httpstate.Backend
if len(installs) > 0 && file == "" {
r, err := httpstate.New(cmdutil.Diag(), httpstate.ValueOrDefaultURL(cloudURL))
r, err := httpstate.New(cmdutil.Diag(), httpstate.ValueOrDefaultURL(cloudURL), "")
if err != nil {
return errors.Wrap(err, "creating API client")
}

View file

@ -117,6 +117,9 @@ func newPreviewCmd() *cobra.Command {
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
cmd.PersistentFlags().StringVar(
&stackConfigFile, "config-file", "",
"Use the configuration values in the specified file rather than detecting the file name")
cmd.PersistentFlags().StringVarP(
&message, "message", "m", "",

View file

@ -128,6 +128,9 @@ func newRefreshCmd() *cobra.Command {
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
cmd.PersistentFlags().StringVar(
&stackConfigFile, "config-file", "",
"Use the configuration values in the specified file rather than detecting the file name")
cmd.PersistentFlags().StringVarP(
&message, "message", "m", "",

View file

@ -74,7 +74,7 @@ func newUpCmd() *cobra.Command {
return err
}
if err = saveConfig(s.Ref().Name(), commandLineConfig); err != nil {
if err = saveConfig(s, commandLineConfig); err != nil {
return errors.Wrap(err, "saving config")
}
}
@ -312,6 +312,9 @@ func newUpCmd() *cobra.Command {
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
cmd.PersistentFlags().StringVar(
&stackConfigFile, "config-file", "",
"Use the configuration values in the specified file rather than detecting the file name")
cmd.PersistentFlags().StringArrayVarP(
&configArray, "config", "c", []string{},
"Config to use during the update")
@ -402,7 +405,7 @@ func handleConfig(
// Save the config.
if c != nil {
if err = saveConfig(s.Ref().Name(), c); err != nil {
if err = saveConfig(s, c); err != nil {
return errors.Wrap(err, "saving config")
}
}

View file

@ -60,9 +60,9 @@ func currentBackend(opts display.Options) (backend.Backend, error) {
return nil, err
}
if filestate.IsLocalBackendURL(creds.Current) {
return filestate.New(cmdutil.Diag(), creds.Current)
return filestate.New(cmdutil.Diag(), creds.Current, stackConfigFile)
}
return httpstate.Login(commandContext(), cmdutil.Diag(), creds.Current, opts)
return httpstate.Login(commandContext(), cmdutil.Diag(), creds.Current, stackConfigFile, opts)
}
// This is used to control the contents of the tracing header.

View file

@ -54,8 +54,9 @@ type Backend interface {
}
type localBackend struct {
d diag.Sink
url string
d diag.Sink
url string
stackConfigFile string
}
type localBackendReference struct {
@ -74,18 +75,19 @@ func IsLocalBackendURL(url string) bool {
return strings.HasPrefix(url, localBackendURLPrefix)
}
func New(d diag.Sink, url string) (Backend, error) {
func New(d diag.Sink, url, stackConfigFile string) (Backend, error) {
if !IsLocalBackendURL(url) {
return nil, errors.Errorf("local URL %s has an illegal prefix; expected %s", url, localBackendURLPrefix)
}
return &localBackend{
d: d,
url: url,
d: d,
url: url,
stackConfigFile: stackConfigFile,
}, nil
}
func Login(d diag.Sink, url string) (Backend, error) {
be, err := New(d, url)
func Login(d diag.Sink, url, stackConfigFile string) (Backend, error) {
be, err := New(d, url, stackConfigFile)
if err != nil {
return nil, err
}
@ -213,7 +215,7 @@ func (b *localBackend) RemoveStack(ctx context.Context, stackRef backend.StackRe
}
func (b *localBackend) GetStackCrypter(stackRef backend.StackReference) (config.Crypter, error) {
return symmetricCrypter(stackRef.Name())
return symmetricCrypter(stackRef.Name(), b.stackConfigFile)
}
func (b *localBackend) GetLatestConfiguration(ctx context.Context,

View file

@ -38,21 +38,29 @@ func readPassphrase(prompt string) (string, error) {
}
// defaultCrypter gets the right value encrypter/decrypter given the project configuration.
func defaultCrypter(stackName tokens.QName, cfg config.Map) (config.Crypter, error) {
func defaultCrypter(stackName tokens.QName, cfg config.Map, configFile string) (config.Crypter, error) {
// If there is no config, we can use a standard panic crypter.
if !cfg.HasSecureValue() {
return config.NewPanicCrypter(), nil
}
// Otherwise, we will use an encrypted one.
return symmetricCrypter(stackName)
return symmetricCrypter(stackName, configFile)
}
// symmetricCrypter gets the right value encrypter/decrypter for this project.
func symmetricCrypter(stackName tokens.QName) (config.Crypter, error) {
func symmetricCrypter(stackName tokens.QName, configFile string) (config.Crypter, error) {
contract.Assertf(stackName != "", "stackName %s", "!= \"\"")
info, err := workspace.DetectProjectStack(stackName)
if configFile == "" {
f, err := workspace.DetectProjectStackPath(stackName)
if err != nil {
return nil, err
}
configFile = f
}
info, err := workspace.LoadProjectStack(configFile)
if err != nil {
return nil, err
}
@ -98,7 +106,7 @@ func symmetricCrypter(stackName tokens.QName) (config.Crypter, error) {
// 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 {
if err = info.Save(configFile); err != nil {
return nil, err
}

View file

@ -86,11 +86,20 @@ func (b *localBackend) newUpdate(stackName tokens.QName, proj *workspace.Project
}
func (b *localBackend) getTarget(stackName tokens.QName) (*deploy.Target, error) {
stk, err := workspace.DetectProjectStack(stackName)
stackConfigFile := b.stackConfigFile
if stackConfigFile == "" {
f, err := workspace.DetectProjectStackPath(stackName)
if err != nil {
return nil, err
}
stackConfigFile = f
}
stk, err := workspace.LoadProjectStack(stackConfigFile)
if err != nil {
return nil, err
}
decrypter, err := defaultCrypter(stackName, stk.Config)
decrypter, err := defaultCrypter(stackName, stk.Config, stackConfigFile)
if err != nil {
return nil, err
}

View file

@ -139,13 +139,14 @@ type Backend interface {
}
type cloudBackend struct {
d diag.Sink
url string
client *client.Client
d diag.Sink
url string
stackConfigFile string
client *client.Client
}
// New creates a new Pulumi backend for the given cloud API URL and token.
func New(d diag.Sink, cloudURL string) (Backend, error) {
func New(d diag.Sink, cloudURL, stackConfigFile string) (Backend, error) {
cloudURL = ValueOrDefaultURL(cloudURL)
apiToken, err := workspace.GetAccessToken(cloudURL)
if err != nil {
@ -153,14 +154,15 @@ func New(d diag.Sink, cloudURL string) (Backend, error) {
}
return &cloudBackend{
d: d,
url: cloudURL,
client: client.NewClient(cloudURL, apiToken, d),
d: d,
url: cloudURL,
stackConfigFile: stackConfigFile,
client: client.NewClient(cloudURL, apiToken, d),
}, nil
}
// loginWithBrowser uses a web-browser to log into the cloud and returns the cloud backend for it.
func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string) (Backend, error) {
func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL, stackConfigFile string) (Backend, error) {
// Locally, we generate a nonce and spin up a web server listening on a random port on localhost. We then open a
// browser to a special endpoint on the Pulumi.com console, passing the generated nonce as well as the port of the
// webserver we launched. This endpoint does the OAuth flow and when it completes, redirects to localhost passing
@ -232,11 +234,11 @@ func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string) (Backen
return nil, err
}
return New(d, cloudURL)
return New(d, cloudURL, stackConfigFile)
}
// Login logs into the target cloud URL and returns the cloud backend for it.
func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error) {
func Login(ctx context.Context, d diag.Sink, cloudURL, stackConfigFile string, opts display.Options) (Backend, error) {
cloudURL = ValueOrDefaultURL(cloudURL)
// If we have a saved access token, and it is valid, use it.
@ -248,7 +250,7 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
return nil, err
}
return New(d, cloudURL)
return New(d, cloudURL, stackConfigFile)
}
}
@ -315,7 +317,7 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
}
if accessToken == "" {
return loginWithBrowser(ctx, d, cloudURL)
return loginWithBrowser(ctx, d, cloudURL, stackConfigFile)
}
}
}
@ -333,7 +335,7 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
return nil, err
}
return New(d, cloudURL)
return New(d, cloudURL, stackConfigFile)
}
func (b *cloudBackend) StackConsoleURL(stackRef backend.StackReference) (string, error) {
@ -653,7 +655,15 @@ func (b *cloudBackend) createAndStartUpdate(
if err != nil {
return client.UpdateIdentifier{}, 0, "", err
}
workspaceStack, err := workspace.DetectProjectStack(stackRef.Name())
stackConfigFile := b.stackConfigFile
if stackConfigFile == "" {
f, err := workspace.DetectProjectStackPath(stackRef.Name())
if err != nil {
return client.UpdateIdentifier{}, 0, "", err
}
stackConfigFile = f
}
workspaceStack, err := workspace.LoadProjectStack(stackConfigFile)
if err != nil {
return client.UpdateIdentifier{}, 0, "", errors.Wrap(err, "getting configuration")
}

View file

@ -290,7 +290,15 @@ func (b *cloudBackend) getSnapshot(ctx context.Context, stackRef backend.StackRe
func (b *cloudBackend) getTarget(ctx context.Context, stackRef backend.StackReference) (*deploy.Target, error) {
// Pull the local stack info so we can get at its configuration bag.
stk, err := workspace.DetectProjectStack(stackRef.Name())
stackConfigFile := b.stackConfigFile
if stackConfigFile == "" {
f, err := workspace.DetectProjectStackPath(stackRef.Name())
if err != nil {
return nil, err
}
stackConfigFile = f
}
stk, err := workspace.LoadProjectStack(stackConfigFile)
if err != nil {
return nil, err
}