pulumi/pkg/backend/local/backend.go
Matt Ellis d3240fdc64 Require pulumi login before commands that need a backend
This change does three major things:

1. Removes the ability to be logged into multiple clouds at the same
time. Previously, we supported being logged into multiple clouds at
the same time and the CLI would fan out requests and join responses
when needed. In general, this was only useful for Pulumi employees
that wanted run against multiple copies of the service (say production
and staging) but overall was very confusing (for example in the old
world a stack with the same identity could appear twice (since it was
in two backends) which the CLI didn't handle very well).

2. Stops treating the "local" backend as a special thing, from the
point of view of the CLI. Previouly we'd always connect to the local
backend and merge that data with whatever was in clouds we were
connected to. We had gestures like `--local` in `pulumi stack init`
that meant "use the local mode". Instead, to use the local mode now
you run `pulumi login --cloud-url local://` and then you are logged in
the local backend. Since you can only ever be logged into a single
backend, we can remove the `--local` and `--remote` flags from `pulumi
stack init`, it just now requires you to be logged in and creates a
stack in whatever back end you were logged into. When logging into the
local backend, you are not prompted for an access key.

3. Prompt for login in places where you have to log in, if you are not
already logged in.
2018-04-05 10:19:41 -07:00

355 lines
9.3 KiB
Go

// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
package local
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/golang/glog"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/apitype"
"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/resource/config"
"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"
)
// localBackendURL is fake URL we use to signal we want to use the local backend vs a cloud one.
const localBackendURL = "local://"
// 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 IsLocalBackendURL(url string) bool {
return url == localBackendURL
}
func New(d diag.Sink) Backend {
return &localBackend{d: d}
}
func Login(d diag.Sink) (Backend, error) {
return &localBackend{d: d}, workspace.StoreAccessToken("local://", "", true)
}
func (b *localBackend) Name() string {
name, _ := os.Hostname()
if name == "" {
name = "local"
}
return name
}
func (b *localBackend) local() {}
func (b *localBackend) CreateStack(stackName tokens.QName, opts interface{}) (backend.Stack, error) {
contract.Requiref(opts == nil, "opts", "local stacks do not support any options")
if stackName == "" {
return nil, errors.New("invalid empty stack name")
}
if _, _, _, err := getStack(stackName); err == nil {
return nil, errors.Errorf("stack '%s' already exists", stackName)
}
file, err := saveStack(stackName, nil, nil)
if err != nil {
return nil, err
}
return newStack(stackName, file, nil, nil, b), nil
}
func (b *localBackend) GetStack(stackName tokens.QName) (backend.Stack, error) {
config, snapshot, path, err := getStack(stackName)
switch {
case os.IsNotExist(errors.Cause(err)):
return nil, nil
case err != nil:
return nil, err
default:
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(stackName)
}
func (b *localBackend) Preview(stackName tokens.QName, proj *workspace.Project, root string, debug bool,
opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
update, err := b.newUpdate(stackName, proj, root)
if err != nil {
return err
}
events := make(chan engine.Event)
done := make(chan bool)
go DisplayEvents("previewing", events, done, debug, displayOpts)
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, proj *workspace.Project, root string,
debug bool, m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return b.performEngineOp(
"updating", backend.DeployUpdate,
stackName, proj, root, debug, m, opts, displayOpts,
func(update *update, events chan engine.Event) (engine.ResourceChanges, error) {
return engine.Update(update, events, opts)
},
)
}
func (b *localBackend) Destroy(stackName tokens.QName, proj *workspace.Project, root string,
debug bool, m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions) error {
return b.performEngineOp(
"destroying", backend.DestroyUpdate,
stackName, proj, root, debug, m, opts, displayOpts,
func(update *update, events chan engine.Event) (engine.ResourceChanges, error) {
return engine.Destroy(update, events, opts)
},
)
}
func (b *localBackend) performEngineOp(op string, kind backend.UpdateKind,
stackName tokens.QName, proj *workspace.Project, root string,
debug bool, m backend.UpdateMetadata, opts engine.UpdateOptions, displayOpts backend.DisplayOptions,
performEngineOp func(*update, chan engine.Event) (engine.ResourceChanges, error)) error {
update, err := b.newUpdate(stackName, proj, root)
if err != nil {
return err
}
events := make(chan engine.Event)
done := make(chan bool)
go DisplayEvents(op, events, done, debug, displayOpts)
// Perform the update
start := time.Now().Unix()
changes, updateErr := performEngineOp(update, events)
end := time.Now().Unix()
<-done
close(events)
close(done)
// Save update results.
result := backend.SucceededResult
if updateErr != nil {
result = backend.FailedResult
}
info := backend.UpdateInfo{
Kind: kind,
StartTime: start,
Message: m.Message,
Environment: m.Environment,
Config: update.GetTarget().Config,
Result: result,
EndTime: end,
// IDEA: it would be nice to populate the *Deployment, so that addToHistory below doens't need to
// rudely assume it knows where the checkpoint file is on disk as it makes a copy of it. This isn't
// trivial to achieve today given the event driven nature of plan-walking, however.
ResourceChanges: changes,
}
var saveErr error
var backupErr error
if !opts.DryRun {
saveErr = addToHistory(stackName, info)
backupErr = backupStack(stackName)
}
if updateErr != nil {
// We swallow saveErr and backupErr as they are less important than the updateErr.
return updateErr
}
if saveErr != nil {
// We swallow backupErr as it is less important than the saveErr.
return errors.Wrap(saveErr, "saving update info")
}
return errors.Wrap(backupErr, "saving backup")
}
func (b *localBackend) GetHistory(stackName tokens.QName) ([]backend.UpdateInfo, error) {
updates, err := getHistory(stackName)
if err != nil {
return nil, err
}
return updates, 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
}
return GetLogsForTarget(target, query)
}
// GetLogsForTarget fetches stack logs using the config, decrypter, and checkpoint in the given target.
func GetLogsForTarget(target *deploy.Target, query operations.LogQuery) ([]operations.LogEntry, error) {
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) (*apitype.UntypedDeployment, 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 &apitype.UntypedDeployment{Deployment: json.RawMessage(data)}, nil
}
func (b *localBackend) ImportDeployment(stackName tokens.QName, deployment *apitype.UntypedDeployment) error {
config, _, _, err := getStack(stackName)
if err != nil {
return err
}
var latest apitype.Deployment
if err = json.Unmarshal(deployment.Deployment, &latest); err != nil {
return err
}
checkpoint := &apitype.CheckpointV1{
Stack: stackName,
Config: config,
Latest: &latest,
}
snap, err := stack.DeserializeCheckpoint(checkpoint)
if err != nil {
return err
}
_, err = saveStack(stackName, config, snap)
return err
}
func (b *localBackend) Logout() error {
return workspace.DeleteAccessToken(localBackendURL)
}
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
}