pulumi/pkg/backend/local/backend.go
Chris Smith 3a3d0698ae
Surface update options to the service (#806)
This PR surfaces the configuration options available to updates, previews, and destroys to the Pulumi Service. As part of this I refactored the options to unify them into a single `engine.UpdateOptions`, since they were all overlapping to various degrees.

With this PR we are adding several new flags to commands, e.g. `--summary` was not available on `pulumi destroy`.

There are also a few minor breaking changes.

- `pulumi destroy --preview` is now `pulumi destroy --dry-run` (to match the actual name of the field).
- The default behavior for "--color" is now `Always`. Previously it was `Always` or `Never` based on the value of a `--debug` flag. (You can specify `--color always` or `--color never` to get the exact behavior.)

Fixes #515, and cleans up the code making some other features slightly easier to add.
2018-01-18 11:10:15 -08:00

278 lines
6.1 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package local
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"github.com/golang/glog"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/encoding"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/pack"
"github.com/pulumi/pulumi/pkg/resource/config"
"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"
)
// Backend extends the base backend interface with specific information about local backends.
type Backend interface {
backend.Backend
local() // at the moment, no local specific info, so just use a marker function.
}
type localBackend struct {
d diag.Sink
}
func New(d diag.Sink) Backend {
return &localBackend{d: d}
}
func (b *localBackend) Name() string {
// nolint: gas
name, _ := os.Hostname()
if name == "" {
name = "local"
}
return name
}
func (b *localBackend) local() {}
func (b *localBackend) CreateStack(stackName tokens.QName, opts interface{}) error {
contract.Requiref(opts == nil, "opts", "local stacks do not support any options")
if _, _, _, err := getStack(stackName); err == nil {
return errors.Errorf("stack '%v' already exists", stackName)
}
return saveStack(stackName, nil, nil)
}
func (b *localBackend) GetStack(stackName tokens.QName) (backend.Stack, error) {
config, snapshot, path, err := getStack(stackName)
if err != nil {
return nil, nil
}
return newStack(stackName, path, config, snapshot, b), nil
}
func (b *localBackend) ListStacks() ([]backend.Stack, error) {
stacks, err := getLocalStacks()
if err != nil {
return nil, err
}
var results []backend.Stack
for _, stackName := range stacks {
stack, err := b.GetStack(stackName)
if err != nil {
return nil, err
}
results = append(results, stack)
}
return results, nil
}
func (b *localBackend) RemoveStack(stackName tokens.QName, force bool) (bool, error) {
_, snapshot, _, err := getStack(stackName)
if err != nil {
return false, err
}
// Don't remove stacks that still have resources.
if !force && snapshot != nil && len(snapshot.Resources) > 0 {
return true, errors.New("refusing to remove stack because it still contains resources")
}
return false, removeStack(stackName)
}
func (b *localBackend) GetStackCrypter(stackName tokens.QName) (config.Crypter, error) {
return symmetricCrypter()
}
func (b *localBackend) Preview(stackName tokens.QName, pkg *pack.Package, root string, debug bool,
opts engine.UpdateOptions) error {
update, err := b.newUpdate(stackName, pkg, root)
if err != nil {
return err
}
events := make(chan engine.Event)
done := make(chan bool)
go displayEvents(events, done, debug)
if err = engine.Preview(update, events, opts); err != nil {
return err
}
<-done
close(events)
close(done)
return nil
}
func (b *localBackend) Update(stackName tokens.QName, pkg *pack.Package, root string, debug bool,
opts engine.UpdateOptions) error {
update, err := b.newUpdate(stackName, pkg, root)
if err != nil {
return err
}
events := make(chan engine.Event)
done := make(chan bool)
go displayEvents(events, done, debug)
if err = engine.Deploy(update, events, opts); err != nil {
return err
}
<-done
close(events)
close(done)
return nil
}
func (b *localBackend) Destroy(stackName tokens.QName, pkg *pack.Package, root string, debug bool,
opts engine.UpdateOptions) error {
update, err := b.newUpdate(stackName, pkg, root)
if err != nil {
return err
}
events := make(chan engine.Event)
done := make(chan bool)
go displayEvents(events, done, debug)
if err := engine.Destroy(update, events, opts); err != nil {
return err
}
<-done
close(events)
close(done)
return nil
}
func (b *localBackend) GetLogs(stackName tokens.QName, query operations.LogQuery) ([]operations.LogEntry, error) {
target, err := b.getTarget(stackName)
if err != nil {
return nil, err
}
contract.Assert(target != nil)
contract.Assert(target.Snapshot != nil)
config, err := target.Config.Decrypt(target.Decrypter)
if err != nil {
return nil, err
}
components := operations.NewResourceTree(target.Snapshot.Resources)
ops := components.OperationsProvider(config)
logs, err := ops.GetLogs(query)
if logs == nil {
return nil, err
}
return *logs, err
}
func (b *localBackend) ExportDeployment(stackName tokens.QName) (json.RawMessage, error) {
_, snap, _, err := getStack(stackName)
if err != nil {
return nil, err
}
data, err := json.Marshal(stack.SerializeDeployment(snap))
if err != nil {
return nil, err
}
return json.RawMessage(data), nil
}
func (b *localBackend) ImportDeployment(stackName tokens.QName, src json.RawMessage) error {
config, _, _, err := getStack(stackName)
if err != nil {
return err
}
var deployment stack.Deployment
if err = json.Unmarshal(src, &deployment); err != nil {
return err
}
checkpoint := &stack.Checkpoint{
Stack: stackName,
Config: config,
Latest: &deployment,
}
snap, err := stack.DeserializeCheckpoint(checkpoint)
if err != nil {
return err
}
return saveStack(stackName, config, snap)
}
func getLocalStacks() ([]tokens.QName, error) {
var stacks []tokens.QName
w, err := workspace.New()
if err != nil {
return nil, err
}
// Read the stack directory.
path := w.StackPath("")
files, err := ioutil.ReadDir(path)
if err != nil && !os.IsNotExist(err) {
return nil, errors.Errorf("could not read stacks: %v", err)
}
for _, file := range files {
// Ignore directories.
if file.IsDir() {
continue
}
// Skip files without valid extensions (e.g., *.bak files).
stackfn := file.Name()
ext := filepath.Ext(stackfn)
if _, has := encoding.Marshalers[ext]; !has {
continue
}
// Read in this stack's information.
name := tokens.QName(stackfn[:len(stackfn)-len(ext)])
_, _, _, err := getStack(name)
if err != nil {
glog.V(5).Infof("error reading stack: %v (%v) skipping", name, err)
continue // failure reading the stack information.
}
stacks = append(stacks, name)
}
return stacks, nil
}