pulumi/pkg/engine/plan.go
Pat Gavlin 2d6579acee
Enhance the engine's tracing support a bit. (#1328)
- Allow callers to provide a parent span for the engine's operations
- Tag each plan context with the name of its associated operation
2018-05-04 17:01:35 -07:00

267 lines
8.7 KiB
Go

// Copyright 2018, Pulumi Corporation. All rights reserved.
package engine
import (
"os"
"github.com/opentracing/opentracing-go"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/workspace"
)
// ProjectInfoContext returns information about the current project, including its pwd, main, and plugin context.
func ProjectInfoContext(projinfo *Projinfo, config plugin.ConfigSource, pluginEvents plugin.Events,
diag diag.Sink, tracingSpan opentracing.Span) (string, string, *plugin.Context, error) {
contract.Require(projinfo != nil, "projinfo")
// If the package contains an override for the main entrypoint, use it.
pwd, main, err := projinfo.GetPwdMain()
if err != nil {
return "", "", nil, err
}
// Create a context for plugins.
ctx, err := plugin.NewContext(diag, nil, config, pluginEvents, pwd, tracingSpan)
if err != nil {
return "", "", nil, err
}
return pwd, main, ctx, nil
}
// newPlanContext creates a context for a subsequent planning operation. Callers must call Close on the
// resulting context object once they have completed the associated planning operation.
func newPlanContext(u UpdateInfo, opName string, parentSpan opentracing.SpanContext) (*planContext, error) {
contract.Require(u != nil, "u")
// Create a root span for the operation
opts := []opentracing.StartSpanOption{}
if opName != "" {
opts = append(opts, opentracing.Tag{Key: "operation", Value: opName})
}
if parentSpan != nil {
opts = append(opts, opentracing.ChildOf(parentSpan))
}
tracingSpan := opentracing.StartSpan("pulumi-plan", opts...)
return &planContext{
Update: u,
TracingSpan: tracingSpan,
}, nil
}
type planContext struct {
Update UpdateInfo // The update being processed.
TracingSpan opentracing.Span // An OpenTracing span to parent plan operations within.
}
func (ctx *planContext) Close() {
ctx.TracingSpan.Finish()
}
// planOptions includes a full suite of options for performing a plan and/or deploy operation.
type planOptions struct {
UpdateOptions
// SourceFunc is a factory that returns an EvalSource to use during planning. This is the thing that
// creates resources to compare against the current checkpoint state (e.g., by evaluating a program, etc).
SourceFunc planSourceFunc
SkipOutputs bool // true if we we should skip printing outputs separately.
DOT bool // true if we should print the DOT file for this plan.
Events eventEmitter // the channel to write events from the engine to.
Diag diag.Sink // the sink to use for diag'ing.
PluginEvents plugin.Events // an optional listener for plugin events
}
// planSourceFunc is a callback that will be used to prepare for, and evaluate, the "new" state for a stack.
type planSourceFunc func(
opts planOptions, proj *workspace.Project, pwd, main string,
target *deploy.Target, plugctx *plugin.Context, dryRun bool) (deploy.Source, error)
// plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan.
func plan(ctx *planContext, opts planOptions, dryRun bool) (*planResult, error) {
contract.Assert(ctx != nil)
contract.Assert(ctx.Update != nil)
contract.Assert(opts.SourceFunc != nil)
// First, load the package metadata and the deployment target in preparation for executing the package's program
// and creating resources. This includes fetching its pwd and main overrides.
proj, target := ctx.Update.GetProject(), ctx.Update.GetTarget()
contract.Assert(proj != nil)
contract.Assert(target != nil)
projinfo := &Projinfo{Proj: proj, Root: ctx.Update.GetRoot()}
pwd, main, plugctx, err := ProjectInfoContext(projinfo, target, opts.PluginEvents, opts.Diag, ctx.TracingSpan)
if err != nil {
return nil, err
}
// Now create the state source. This may issue an error if it can't create the source. This entails,
// for example, loading any plugins which will be required to execute a program, among other things.
source, err := opts.SourceFunc(opts, proj, pwd, main, target, plugctx, dryRun)
if err != nil {
return nil, err
}
// If there are any analyzers in the project file, add them.
var analyzers []tokens.QName
if as := projinfo.Proj.Analyzers; as != nil {
for _, a := range *as {
analyzers = append(analyzers, a)
}
}
// Append any analyzers from the command line.
for _, a := range opts.Analyzers {
analyzers = append(analyzers, tokens.QName(a))
}
// Generate a plan; this API handles all interesting cases (create, update, delete).
plan := deploy.NewPlan(plugctx, target, target.Snapshot, source, analyzers, dryRun)
return &planResult{
Ctx: ctx,
Plugctx: plugctx,
Plan: plan,
Options: opts,
}, nil
}
type planResult struct {
Ctx *planContext // plan context information.
Plugctx *plugin.Context // the context containing plugins and their state.
Plan *deploy.Plan // the plan created by this command.
Options planOptions // the options used during planning.
}
// Chdir changes the directory so that all operations from now on are relative to the project we are working with.
// It returns a function that, when run, restores the old working directory.
func (res *planResult) Chdir() (func(), error) {
pwd := res.Plugctx.Pwd
if pwd == "" {
return func() {}, nil
}
oldpwd, err := os.Getwd()
if err != nil {
return nil, err
}
if err = os.Chdir(pwd); err != nil {
return nil, errors.Wrapf(err, "could not change to the project working directory")
}
return func() {
// Restore the working directory after planning completes.
cderr := os.Chdir(oldpwd)
contract.IgnoreError(cderr)
}, nil
}
// Walk enumerates all steps in the plan, calling out to the provided action at each step. It returns four things: the
// resulting Snapshot, no matter whether an error occurs or not; an error, if something went wrong; the step that
// failed, if the error is non-nil; and finally the state of the resource modified in the failing step.
func (res *planResult) Walk(ctx *Context, events deploy.Events, preview bool) (deploy.PlanSummary,
deploy.Step, resource.Status, error) {
opts := deploy.Options{
Events: events,
Parallel: res.Options.Parallel,
}
// Fetch a plan iterator and keep walking it until we are done.
iter, err := res.Plan.Start(opts)
if err != nil {
return nil, nil, resource.StatusOK, err
}
step, err := iter.Next()
if err != nil {
closeErr := iter.Close() // ignore close errors; the Next error trumps
contract.IgnoreError(closeErr)
return nil, nil, resource.StatusOK, err
}
// Iterate the plan in a goroutine while listening for termination.
var rst resource.Status
done := make(chan bool)
go func() {
defer func() {
// Close the iterator. If we have already observed another error, that error trumps the close error.
closeErr := iter.Close()
if err == nil {
err = closeErr
}
close(done)
}()
for step != nil {
// Check for cancellation and termination.
if cancelErr := ctx.Cancel.CancelErr(); cancelErr != nil {
rst, err = resource.StatusOK, cancelErr
return
}
// Perform any per-step actions.
rst, err = iter.Apply(step, preview)
// If an error occurred, exit early.
if err != nil {
return
}
contract.Assert(rst == resource.StatusOK)
step, err = iter.Next()
if err != nil {
return
}
}
// Finally, return a summary and the resulting plan information.
rst, err = resource.StatusOK, nil
}()
select {
case <-ctx.Cancel.Terminated():
return iter, step, rst, ctx.Cancel.TerminateErr()
case <-done:
return iter, step, rst, err
}
}
func (res *planResult) Close() error {
return res.Plugctx.Close()
}
// printPlan prints the plan's result to the plan's Options.Events stream.
func printPlan(ctx *Context, result *planResult, dryRun bool) (ResourceChanges, error) {
result.Options.Events.preludeEvent(dryRun, result.Ctx.Update.GetTarget().Config)
// Walk the plan's steps and and pretty-print them out.
actions := newPreviewActions(result.Options)
_, step, _, err := result.Walk(ctx, actions, true)
if err != nil {
var failedUrn resource.URN
if step != nil {
failedUrn = step.URN()
}
result.Options.Diag.Errorf(diag.Message(failedUrn, err.Error()))
return nil, errors.New("an error occurred while advancing the preview")
}
// Emit an event with a summary of operation counts.
changes := ResourceChanges(actions.Ops)
result.Options.Events.previewSummaryEvent(changes)
return changes, nil
}
func assertSeen(seen map[resource.URN]deploy.Step, step deploy.Step) {
_, has := seen[step.URN()]
contract.Assertf(has, "URN '%v' had not been marked as seen", step.URN())
}