pulumi/cmd/provider_local.go
Matt Ellis e361098941 Support global configuration
Previously, config information was stored per stack. With this change,
we now allow config values which apply to every stack a program may
target.

When passed without the `-s <stack>` argument, `pulumi config`
operates on the "global" configuration. Stack specific information can
be modified by passing an explicit stack.

Stack specific configuration overwrites global configuration.

Conside the following Pulumi.yaml:

```
name: hello-world
runtime: nodejs
description: a hello world program
config:
  hello-world:config:message Hello, from Pulumi
stacks:
  production:
    config:
      hello-world:config:message Hello, from Production
```

This program contains a single configuration value,
"hello-world:config:message" which has the value "Hello, from Pulumi"
when the program is activated into any stack except for "production"
where the value is "Hello, from Production".
2017-10-20 13:30:07 -07:00

139 lines
3.7 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/encoding"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/resource/stack"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/workspace"
)
type localStackProvider struct{}
func (p localStackProvider) GetTarget(name tokens.QName) (*deploy.Target, error) {
contract.Require(name != "", "name")
target, _, err := getStack(name)
if err != nil {
return nil, err
}
config, err := getConfiguration(name)
if err != nil {
return nil, err
}
target.Config = config
return target, err
}
func (p localStackProvider) GetSnapshot(name tokens.QName) (*deploy.Snapshot, error) {
contract.Require(name != "", "name")
_, snapshot, err := getStack(name)
return snapshot, err
}
func (p localStackProvider) SaveSnapshot(snapshot *deploy.Snapshot) error {
target, _, err := getStack(snapshot.Namespace)
if err != nil && !os.IsNotExist(err) {
return err
}
if target == nil {
target = &deploy.Target{Name: snapshot.Namespace}
}
return saveStack(target, snapshot)
}
func getStack(name tokens.QName) (*deploy.Target, *deploy.Snapshot, error) {
contract.Require(name != "", "name")
file := workspace.StackPath(name)
// Detect the encoding of the file so we can do our initial unmarshaling.
m, ext := encoding.Detect(file)
if m == nil {
return nil, nil, errors.Errorf("resource deserialization failed; illegal markup extension: '%v'", ext)
}
// Now read the whole file into a byte blob.
b, err := ioutil.ReadFile(file)
if err != nil {
if os.IsNotExist(err) {
return nil, nil, err
}
return nil, nil, err
}
// Unmarshal the contents into a checkpoint structure.
var checkpoint stack.Checkpoint
if err = m.Unmarshal(b, &checkpoint); err != nil {
return nil, nil, err
}
target, snapshot := stack.DeserializeCheckpoint(&checkpoint)
contract.Assert(target != nil)
return target, snapshot, nil
}
func saveStack(target *deploy.Target, snap *deploy.Snapshot) error {
contract.Require(target != nil, "target")
file := workspace.StackPath(target.Name)
// Make a serializable stack and then use the encoder to encode it.
m, ext := encoding.Detect(file)
if m == nil {
return errors.Errorf("resource serialization failed; illegal markup extension: '%v'", ext)
}
if filepath.Ext(file) == "" {
file = file + ext
}
dep := stack.SerializeCheckpoint(target, snap)
b, err := m.Marshal(dep)
if err != nil {
return errors.Wrap(err, "An IO error occurred during the current operation")
}
// Back up the existing file if it already exists.
backupTarget(file)
// Ensure the directory exists.
if err = os.MkdirAll(filepath.Dir(file), 0700); err != nil {
return errors.Wrap(err, "An IO error occurred during the current operation")
}
// And now write out the new snapshot file, overwriting that location.
if err = ioutil.WriteFile(file, b, 0600); err != nil {
return errors.Wrap(err, "An IO error occurred during the current operation")
}
return nil
}
func removeStack(stack *deploy.Target) error {
contract.Require(stack != nil, "stack")
// Just make a backup of the file and don't write out anything new.
file := workspace.StackPath(stack.Name)
backupTarget(file)
return nil
}
// backupTarget makes a backup of an existing file, in preparation for writing a new one. Instead of a copy, it
// simply renames the file, which is simpler, more efficient, etc.
func backupTarget(file string) {
contract.Require(file != "", "file")
err := os.Rename(file, file+".bak")
contract.IgnoreError(err) // ignore errors.
// IDEA: consider multiple backups (.bak.bak.bak...etc).
}