// 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 }