This changes two things: 1) Eliminates the fact that we had two kinds of previews in our engine. 2) Always initialize the plugin.Events, to ensure that all plugin loads are persisted no matter the update type (update, refresh, destroy), and skip initializing it when dryRun == true, since we won't save them.
434 lines
13 KiB
Go
434 lines
13 KiB
Go
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
|
|
|
package local
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"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/util/logging"
|
|
"github.com/pulumi/pulumi/pkg/workspace"
|
|
)
|
|
|
|
// localBackendURL is fake URL scheme we use to signal we want to use the local backend vs a cloud one.
|
|
const localBackendURLPrefix = "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
|
|
url string
|
|
stateRoot string
|
|
}
|
|
|
|
type localBackendReference struct {
|
|
name tokens.QName
|
|
}
|
|
|
|
func (r localBackendReference) String() string {
|
|
return string(r.name)
|
|
}
|
|
|
|
func (r localBackendReference) StackName() tokens.QName {
|
|
return r.name
|
|
}
|
|
|
|
func stateRootFromLocalURL(localURL string) string {
|
|
if localURL == localBackendURLPrefix {
|
|
user, err := user.Current()
|
|
contract.AssertNoErrorf(err, "could not determine current user")
|
|
return filepath.Join(user.HomeDir, workspace.BookkeepingDir)
|
|
}
|
|
|
|
return localURL[len(localBackendURLPrefix):]
|
|
}
|
|
|
|
func IsLocalBackendURL(url string) bool {
|
|
return strings.HasPrefix(url, localBackendURLPrefix)
|
|
}
|
|
|
|
func New(d diag.Sink, localURL string) Backend {
|
|
return &localBackend{d: d, url: localURL, stateRoot: stateRootFromLocalURL(localURL)}
|
|
}
|
|
|
|
func Login(d diag.Sink, localURL string) (Backend, error) {
|
|
return New(d, localURL), workspace.StoreAccessToken(localURL, "", true)
|
|
}
|
|
|
|
func (b *localBackend) Name() string {
|
|
name, err := os.Hostname()
|
|
contract.IgnoreError(err)
|
|
if name == "" {
|
|
name = "local"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func (b *localBackend) ParseStackReference(stackRefName string) (backend.StackReference, error) {
|
|
return localBackendReference{name: tokens.QName(stackRefName)}, nil
|
|
}
|
|
|
|
func (b *localBackend) local() {}
|
|
|
|
func (b *localBackend) CreateStack(ctx context.Context, stackRef backend.StackReference,
|
|
opts interface{}) (backend.Stack, error) {
|
|
|
|
contract.Requiref(opts == nil, "opts", "local stacks do not support any options")
|
|
|
|
stackName := stackRef.StackName()
|
|
if stackName == "" {
|
|
return nil, errors.New("invalid empty stack name")
|
|
}
|
|
|
|
if _, _, _, err := b.getStack(stackName); err == nil {
|
|
return nil, &backend.StackAlreadyExistsError{StackName: string(stackName)}
|
|
}
|
|
|
|
tags, err := backend.GetStackTags()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting stack tags")
|
|
}
|
|
if err = backend.ValidateStackProperties(string(stackName), tags); err != nil {
|
|
return nil, errors.Wrap(err, "validating stack properties")
|
|
}
|
|
|
|
file, err := b.saveStack(stackName, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stack := newStack(stackRef, file, nil, nil, b)
|
|
fmt.Printf("Created stack '%s'.\n", stack.Name())
|
|
|
|
return stack, nil
|
|
}
|
|
|
|
func (b *localBackend) GetStack(ctx context.Context, stackRef backend.StackReference) (backend.Stack, error) {
|
|
stackName := stackRef.StackName()
|
|
config, snapshot, path, err := b.getStack(stackName)
|
|
switch {
|
|
case os.IsNotExist(errors.Cause(err)):
|
|
return nil, nil
|
|
case err != nil:
|
|
return nil, err
|
|
default:
|
|
return newStack(stackRef, path, config, snapshot, b), nil
|
|
}
|
|
}
|
|
|
|
func (b *localBackend) ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]backend.Stack, error) {
|
|
stacks, err := b.getLocalStacks()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var results []backend.Stack
|
|
for _, stackName := range stacks {
|
|
stack, err := b.GetStack(ctx, localBackendReference{name: stackName})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, stack)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (b *localBackend) RemoveStack(ctx context.Context, stackRef backend.StackReference, force bool) (bool, error) {
|
|
stackName := stackRef.StackName()
|
|
_, snapshot, _, err := b.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, b.removeStack(stackName)
|
|
}
|
|
|
|
func (b *localBackend) GetStackCrypter(stackRef backend.StackReference) (config.Crypter, error) {
|
|
return symmetricCrypter(stackRef.StackName())
|
|
}
|
|
|
|
func (b *localBackend) GetLatestConfiguration(ctx context.Context,
|
|
stackRef backend.StackReference) (config.Map, error) {
|
|
|
|
hist, err := b.GetHistory(ctx, stackRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(hist) == 0 {
|
|
return nil, errors.New("no previous deployment")
|
|
}
|
|
|
|
return hist[0].Config, nil
|
|
}
|
|
|
|
func (b *localBackend) Preview(
|
|
_ context.Context, stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
|
|
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) (engine.ResourceChanges, error) {
|
|
return b.performEngineOp("previewing", backend.PreviewUpdate,
|
|
stackRef.StackName(), proj, root, m, opts, scopes, engine.Update)
|
|
}
|
|
|
|
func (b *localBackend) Update(
|
|
_ context.Context, stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
|
|
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) (engine.ResourceChanges, error) {
|
|
|
|
// The Pulumi Service will pick up changes to a stack's tags on each update. (e.g. changing the description
|
|
// in Pulumi.yaml.) While this isn't necessary for local updates, we do the validation here to keep
|
|
// parity with stacks managed by the Pulumi Service.
|
|
tags, err := backend.GetStackTags()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting stack tags")
|
|
}
|
|
stackName := stackRef.StackName()
|
|
if err = backend.ValidateStackProperties(string(stackName), tags); err != nil {
|
|
return nil, errors.Wrap(err, "validating stack properties")
|
|
}
|
|
return b.performEngineOp("updating", backend.DeployUpdate,
|
|
stackName, proj, root, m, opts, scopes, engine.Update)
|
|
}
|
|
|
|
func (b *localBackend) Refresh(
|
|
_ context.Context, stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
|
|
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) (engine.ResourceChanges, error) {
|
|
return b.performEngineOp("refreshing", backend.RefreshUpdate,
|
|
stackRef.StackName(), proj, root, m, opts, scopes, engine.Refresh)
|
|
}
|
|
|
|
func (b *localBackend) Destroy(
|
|
_ context.Context, stackRef backend.StackReference, proj *workspace.Project, root string, m backend.UpdateMetadata,
|
|
opts backend.UpdateOptions, scopes backend.CancellationScopeSource) (engine.ResourceChanges, error) {
|
|
return b.performEngineOp("destroying", backend.DestroyUpdate,
|
|
stackRef.StackName(), proj, root, m, opts, scopes, engine.Destroy)
|
|
}
|
|
|
|
type engineOpFunc func(engine.UpdateInfo, *engine.Context, engine.UpdateOptions, bool) (engine.ResourceChanges, error)
|
|
|
|
func (b *localBackend) performEngineOp(op string, kind backend.UpdateKind,
|
|
stackName tokens.QName, proj *workspace.Project, root string, m backend.UpdateMetadata, opts backend.UpdateOptions,
|
|
scopes backend.CancellationScopeSource, performEngineOp engineOpFunc) (engine.ResourceChanges, error) {
|
|
|
|
update, err := b.newUpdate(stackName, proj, root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
events := make(chan engine.Event)
|
|
dryRun := (kind == backend.PreviewUpdate)
|
|
|
|
cancelScope := scopes.NewScope(events, dryRun)
|
|
defer cancelScope.Close()
|
|
|
|
done := make(chan bool)
|
|
go DisplayEvents(op, events, done, opts.Display)
|
|
|
|
// Create the management machinery.
|
|
persister := b.newSnapshotPersister(stackName)
|
|
manager := backend.NewSnapshotManager(persister, update.GetTarget().Snapshot)
|
|
engineCtx := &engine.Context{Cancel: cancelScope.Context(), Events: events, SnapshotManager: manager}
|
|
|
|
// Perform the update
|
|
start := time.Now().Unix()
|
|
changes, updateErr := performEngineOp(update, engineCtx, opts.Engine, dryRun)
|
|
end := time.Now().Unix()
|
|
|
|
<-done
|
|
close(events)
|
|
close(done)
|
|
contract.IgnoreClose(manager)
|
|
|
|
// 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 !dryRun {
|
|
saveErr = b.addToHistory(stackName, info)
|
|
backupErr = b.backupStack(stackName)
|
|
}
|
|
|
|
if updateErr != nil {
|
|
// We swallow saveErr and backupErr as they are less important than the updateErr.
|
|
return changes, updateErr
|
|
}
|
|
if saveErr != nil {
|
|
// We swallow backupErr as it is less important than the saveErr.
|
|
return changes, errors.Wrap(saveErr, "saving update info")
|
|
}
|
|
return changes, errors.Wrap(backupErr, "saving backup")
|
|
}
|
|
|
|
func (b *localBackend) GetHistory(ctx context.Context, stackRef backend.StackReference) ([]backend.UpdateInfo, error) {
|
|
stackName := stackRef.StackName()
|
|
updates, err := b.getHistory(stackName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func (b *localBackend) GetLogs(ctx context.Context, stackRef backend.StackReference,
|
|
query operations.LogQuery) ([]operations.LogEntry, error) {
|
|
|
|
stackName := stackRef.StackName()
|
|
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(ctx context.Context,
|
|
stackRef backend.StackReference) (*apitype.UntypedDeployment, error) {
|
|
|
|
stackName := stackRef.StackName()
|
|
_, snap, _, err := b.getStack(stackName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := json.Marshal(stack.SerializeDeployment(snap))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &apitype.UntypedDeployment{
|
|
Version: 1,
|
|
Deployment: json.RawMessage(data),
|
|
}, nil
|
|
}
|
|
|
|
func (b *localBackend) ImportDeployment(ctx context.Context, stackRef backend.StackReference,
|
|
deployment *apitype.UntypedDeployment) error {
|
|
|
|
stackName := stackRef.StackName()
|
|
config, _, _, err := b.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 = b.saveStack(stackName, config, snap)
|
|
return err
|
|
}
|
|
|
|
func (b *localBackend) Logout() error {
|
|
return workspace.DeleteAccessToken(b.url)
|
|
}
|
|
|
|
func (b *localBackend) getLocalStacks() ([]tokens.QName, error) {
|
|
var stacks []tokens.QName
|
|
|
|
// Read the stack directory.
|
|
path := b.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 := b.getStack(name)
|
|
if err != nil {
|
|
logging.V(5).Infof("error reading stack: %v (%v) skipping", name, err)
|
|
continue // failure reading the stack information.
|
|
}
|
|
|
|
stacks = append(stacks, name)
|
|
}
|
|
|
|
return stacks, nil
|
|
}
|