From e101f808391481f1afcad3e038a275fb9b3aaeac Mon Sep 17 00:00:00 2001 From: Pat Gavlin Date: Mon, 16 Nov 2020 10:41:31 -0800 Subject: [PATCH] Plumb plans through the CLI. --- pkg/backend/apply.go | 20 +++-- pkg/backend/backend.go | 2 +- pkg/backend/filestate/backend.go | 25 ++++--- pkg/backend/filestate/stack.go | 2 +- pkg/backend/httpstate/backend.go | 23 +++--- pkg/backend/httpstate/stack.go | 2 +- pkg/backend/mock.go | 8 +- pkg/backend/stack.go | 4 +- pkg/backend/watch.go | 2 +- pkg/cmd/pulumi/preview.go | 19 ++++- pkg/cmd/pulumi/up.go | 23 ++++++ pkg/cmd/pulumi/util.go | 29 ++++++++ pkg/resource/deploy/step_generator.go | 4 + pkg/resource/stack/plan.go | 102 ++++++++++++++++++++++++++ sdk/go/common/apitype/plan.go | 56 ++++++++++++++ 15 files changed, 279 insertions(+), 42 deletions(-) create mode 100644 pkg/resource/stack/plan.go create mode 100644 sdk/go/common/apitype/plan.go diff --git a/pkg/backend/apply.go b/pkg/backend/apply.go index fd9d11c76..cbe3bdc69 100644 --- a/pkg/backend/apply.go +++ b/pkg/backend/apply.go @@ -45,7 +45,7 @@ type ApplierOptions struct { // Applier applies the changes specified by this update operation against the target stack. type Applier func(ctx context.Context, kind apitype.UpdateKind, stack Stack, op UpdateOperation, - opts ApplierOptions, events chan<- engine.Event) (engine.ResourceChanges, result.Result) + opts ApplierOptions, events chan<- engine.Event) (engine.Plan, engine.ResourceChanges, result.Result) func ActionLabel(kind apitype.UpdateKind, dryRun bool) string { v := updateTextMap[kind] @@ -80,7 +80,7 @@ const ( ) func PreviewThenPrompt(ctx context.Context, kind apitype.UpdateKind, stack Stack, - op UpdateOperation, apply Applier) (engine.ResourceChanges, result.Result) { + op UpdateOperation, apply Applier) (engine.Plan, engine.ResourceChanges, result.Result) { // create a channel to hear about the update events from the engine. this will be used so that // we can build up the diff display in case the user asks to see the details of the diff @@ -112,22 +112,22 @@ func PreviewThenPrompt(ctx context.Context, kind apitype.UpdateKind, stack Stack ShowLink: true, } - changes, res := apply(ctx, kind, stack, op, opts, eventsChannel) + plan, changes, res := apply(ctx, kind, stack, op, opts, eventsChannel) if res != nil { close(eventsChannel) - return changes, res + return plan, changes, res } // If there are no changes, or we're auto-approving or just previewing, we can skip the confirmation prompt. if op.Opts.AutoApprove || kind == apitype.PreviewUpdate { close(eventsChannel) - return changes, nil + return plan, changes, nil } // Otherwise, ensure the user wants to proceed. res = confirmBeforeUpdating(kind, stack, events, op.Opts) close(eventsChannel) - return changes, res + return plan, changes, res } // confirmBeforeUpdating asks the user whether to proceed. A nil error means yes. @@ -197,10 +197,13 @@ func PreviewThenPromptThenExecute(ctx context.Context, kind apitype.UpdateKind, // Preview the operation to the user and ask them if they want to proceed. if !op.Opts.SkipPreview { - changes, res := PreviewThenPrompt(ctx, kind, stack, op, apply) + plan, changes, res := PreviewThenPrompt(ctx, kind, stack, op, apply) if res != nil || kind == apitype.PreviewUpdate { return changes, res } + + // TODO(pdg-plan): should this check for an existing plan? + op.Opts.Engine.Plan = plan } // Perform the change (!DryRun) and show the cloud link to the result. @@ -209,7 +212,8 @@ func PreviewThenPromptThenExecute(ctx context.Context, kind apitype.UpdateKind, DryRun: false, ShowLink: true, } - return apply(ctx, kind, stack, op, opts, nil /*events*/) + _, changes, res := apply(ctx, kind, stack, op, opts, nil /*events*/) + return changes, res } func createDiff(updateKind apitype.UpdateKind, events []engine.Event, displayOpts display.Options) string { diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go index 52589d7f1..464dfd81d 100644 --- a/pkg/backend/backend.go +++ b/pkg/backend/backend.go @@ -162,7 +162,7 @@ type Backend interface { RenameStack(ctx context.Context, stack Stack, newName tokens.QName) (StackReference, error) // Preview shows what would be updated given the current workspace's contents. - Preview(ctx context.Context, stack Stack, op UpdateOperation) (engine.ResourceChanges, result.Result) + Preview(ctx context.Context, stack Stack, op UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) // Update updates the target stack with the current workspace's contents (config and code). Update(ctx context.Context, stack Stack, op UpdateOperation) (engine.ResourceChanges, result.Result) // Import imports resources into a stack. diff --git a/pkg/backend/filestate/backend.go b/pkg/backend/filestate/backend.go index 5603b623e..6fbff6edd 100644 --- a/pkg/backend/filestate/backend.go +++ b/pkg/backend/filestate/backend.go @@ -459,7 +459,7 @@ func (b *localBackend) PackPolicies( } func (b *localBackend) Preview(ctx context.Context, stack backend.Stack, - op backend.UpdateOperation) (engine.ResourceChanges, result.Result) { + op backend.UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) { if cmdutil.IsTruthy(os.Getenv(PulumiFilestateLockingEnvVar)) { err := b.Lock(ctx, stack.Ref()) @@ -546,7 +546,7 @@ func (b *localBackend) Watch(ctx context.Context, stack backend.Stack, func (b *localBackend) apply( ctx context.Context, kind apitype.UpdateKind, stack backend.Stack, op backend.UpdateOperation, opts backend.ApplierOptions, - events chan<- engine.Event) (engine.ResourceChanges, result.Result) { + events chan<- engine.Event) (engine.Plan, engine.ResourceChanges, result.Result) { stackRef := stack.Ref() stackName := stackRef.Name() @@ -561,7 +561,7 @@ func (b *localBackend) apply( // Start the update. update, err := b.newUpdate(stackName, op) if err != nil { - return nil, result.FromError(err) + return nil, nil, result.FromError(err) } // Spawn a display loop to show events on the CLI. @@ -602,19 +602,20 @@ func (b *localBackend) apply( // Perform the update start := time.Now().Unix() + var plan engine.Plan var changes engine.ResourceChanges var updateRes result.Result switch kind { case apitype.PreviewUpdate: - changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, true) + plan, changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, true) case apitype.UpdateUpdate: - changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, opts.DryRun) + _, changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, opts.DryRun) case apitype.ResourceImportUpdate: - changes, updateRes = engine.Import(update, engineCtx, op.Opts.Engine, op.Imports, opts.DryRun) + _, changes, updateRes = engine.Import(update, engineCtx, op.Opts.Engine, op.Imports, opts.DryRun) case apitype.RefreshUpdate: - changes, updateRes = engine.Refresh(update, engineCtx, op.Opts.Engine, opts.DryRun) + _, changes, updateRes = engine.Refresh(update, engineCtx, op.Opts.Engine, opts.DryRun) case apitype.DestroyUpdate: - changes, updateRes = engine.Destroy(update, engineCtx, op.Opts.Engine, opts.DryRun) + _, changes, updateRes = engine.Destroy(update, engineCtx, op.Opts.Engine, opts.DryRun) default: contract.Failf("Unrecognized update kind: %s", kind) } @@ -658,16 +659,16 @@ func (b *localBackend) apply( if updateRes != nil { // We swallow saveErr and backupErr as they are less important than the updateErr. - return changes, updateRes + return plan, changes, updateRes } if saveErr != nil { // We swallow backupErr as it is less important than the saveErr. - return changes, result.FromError(errors.Wrap(saveErr, "saving update info")) + return plan, changes, result.FromError(errors.Wrap(saveErr, "saving update info")) } if backupErr != nil { - return changes, result.FromError(errors.Wrap(backupErr, "saving backup")) + return plan, changes, result.FromError(errors.Wrap(backupErr, "saving backup")) } // Make sure to print a link to the stack's checkpoint before exiting. @@ -703,7 +704,7 @@ func (b *localBackend) apply( } } - return changes, nil + return plan, changes, nil } // query executes a query program against the resource outputs of a locally hosted stack. diff --git a/pkg/backend/filestate/stack.go b/pkg/backend/filestate/stack.go index bb99141c3..e9ab317f4 100644 --- a/pkg/backend/filestate/stack.go +++ b/pkg/backend/filestate/stack.go @@ -64,7 +64,7 @@ func (s *localStack) Rename(ctx context.Context, newName tokens.QName) (backend. return backend.RenameStack(ctx, s, newName) } -func (s *localStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.ResourceChanges, result.Result) { +func (s *localStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) { return backend.PreviewStack(ctx, s, op) } diff --git a/pkg/backend/httpstate/backend.go b/pkg/backend/httpstate/backend.go index ed4786f4f..8c4b260b2 100644 --- a/pkg/backend/httpstate/backend.go +++ b/pkg/backend/httpstate/backend.go @@ -824,7 +824,7 @@ func (b *cloudBackend) RenameStack(ctx context.Context, stack backend.Stack, } func (b *cloudBackend) Preview(ctx context.Context, stack backend.Stack, - op backend.UpdateOperation) (engine.ResourceChanges, result.Result) { + op backend.UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) { // We can skip PreviewtThenPromptThenExecute, and just go straight to Execute. opts := backend.ApplierOptions{ DryRun: true, @@ -927,7 +927,7 @@ func (b *cloudBackend) createAndStartUpdate( func (b *cloudBackend) apply( ctx context.Context, kind apitype.UpdateKind, stack backend.Stack, op backend.UpdateOperation, opts backend.ApplierOptions, - events chan<- engine.Event) (engine.ResourceChanges, result.Result) { + events chan<- engine.Event) (engine.Plan, engine.ResourceChanges, result.Result) { actionLabel := backend.ActionLabel(kind, opts.DryRun) @@ -941,7 +941,7 @@ func (b *cloudBackend) apply( update, version, token, err := b.createAndStartUpdate(ctx, kind, stack, &op, opts.DryRun) if err != nil { - return nil, result.FromError(err) + return nil, nil, result.FromError(err) } if !op.Opts.Display.SuppressPermalink && opts.ShowLink && !op.Opts.Display.JSONDisplay { @@ -981,12 +981,12 @@ func (b *cloudBackend) query(ctx context.Context, op backend.QueryOperation, func (b *cloudBackend) runEngineAction( ctx context.Context, kind apitype.UpdateKind, stackRef backend.StackReference, op backend.UpdateOperation, update client.UpdateIdentifier, token string, - callerEventsOpt chan<- engine.Event, dryRun bool) (engine.ResourceChanges, result.Result) { + callerEventsOpt chan<- engine.Event, dryRun bool) (engine.Plan, engine.ResourceChanges, result.Result) { contract.Assertf(token != "", "persisted actions require a token") u, err := b.newUpdate(ctx, stackRef, op, update, token) if err != nil { - return nil, result.FromError(err) + return nil, nil, result.FromError(err) } // displayEvents renders the event to the console and Pulumi service. The processor for the @@ -1035,19 +1035,20 @@ func (b *cloudBackend) runEngineAction( engineCtx.ParentSpan = parentSpan.Context() } + var plan engine.Plan var changes engine.ResourceChanges var res result.Result switch kind { case apitype.PreviewUpdate: - changes, res = engine.Update(u, engineCtx, op.Opts.Engine, true) + plan, changes, res = engine.Update(u, engineCtx, op.Opts.Engine, true) case apitype.UpdateUpdate: - changes, res = engine.Update(u, engineCtx, op.Opts.Engine, dryRun) + _, changes, res = engine.Update(u, engineCtx, op.Opts.Engine, dryRun) case apitype.ResourceImportUpdate: - changes, res = engine.Import(u, engineCtx, op.Opts.Engine, op.Imports, dryRun) + _, changes, res = engine.Import(u, engineCtx, op.Opts.Engine, op.Imports, dryRun) case apitype.RefreshUpdate: - changes, res = engine.Refresh(u, engineCtx, op.Opts.Engine, dryRun) + _, changes, res = engine.Refresh(u, engineCtx, op.Opts.Engine, dryRun) case apitype.DestroyUpdate: - changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun) + _, changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun) default: contract.Failf("Unrecognized update kind: %s", kind) } @@ -1073,7 +1074,7 @@ func (b *cloudBackend) runEngineAction( res = result.Merge(res, result.FromError(errors.Wrap(completeErr, "failed to complete update"))) } - return changes, res + return plan, changes, res } func (b *cloudBackend) CancelCurrentUpdate(ctx context.Context, stackRef backend.StackReference) error { diff --git a/pkg/backend/httpstate/stack.go b/pkg/backend/httpstate/stack.go index bb6fddc80..5a276b9e7 100644 --- a/pkg/backend/httpstate/stack.go +++ b/pkg/backend/httpstate/stack.go @@ -140,7 +140,7 @@ func (s *cloudStack) Rename(ctx context.Context, newName tokens.QName) (backend. return backend.RenameStack(ctx, s, newName) } -func (s *cloudStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.ResourceChanges, result.Result) { +func (s *cloudStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) { return backend.PreviewStack(ctx, s, op) } diff --git a/pkg/backend/mock.go b/pkg/backend/mock.go index ca05f268b..1622147ca 100644 --- a/pkg/backend/mock.go +++ b/pkg/backend/mock.go @@ -57,7 +57,7 @@ type MockBackend struct { LogoutAllF func() error CurrentUserF func() (string, error) PreviewF func(context.Context, Stack, - UpdateOperation) (engine.ResourceChanges, result.Result) + UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) UpdateF func(context.Context, Stack, UpdateOperation) (engine.ResourceChanges, result.Result) ImportF func(context.Context, Stack, @@ -180,7 +180,7 @@ func (be *MockBackend) GetStackCrypter(stackRef StackReference) (config.Crypter, } func (be *MockBackend) Preview(ctx context.Context, stack Stack, - op UpdateOperation) (engine.ResourceChanges, result.Result) { + op UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) { if be.PreviewF != nil { return be.PreviewF(ctx, stack, op) @@ -335,7 +335,7 @@ type MockStack struct { ConfigF func() config.Map SnapshotF func(ctx context.Context) (*deploy.Snapshot, error) BackendF func() Backend - PreviewF func(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result) + PreviewF func(ctx context.Context, op UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) UpdateF func(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result) ImportF func(ctx context.Context, op UpdateOperation, imports []deploy.Import) (engine.ResourceChanges, result.Result) @@ -381,7 +381,7 @@ func (ms *MockStack) Backend() Backend { panic("not implemented") } -func (ms *MockStack) Preview(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result) { +func (ms *MockStack) Preview(ctx context.Context, op UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) { if ms.PreviewF != nil { return ms.PreviewF(ctx, op) } diff --git a/pkg/backend/stack.go b/pkg/backend/stack.go index ad33c1074..b3f260552 100644 --- a/pkg/backend/stack.go +++ b/pkg/backend/stack.go @@ -40,7 +40,7 @@ type Stack interface { Backend() Backend // the backend this stack belongs to. // Preview changes to this stack. - Preview(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result) + Preview(ctx context.Context, op UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) // Update this stack. Update(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result) // Import resources into this stack. @@ -75,7 +75,7 @@ func RenameStack(ctx context.Context, s Stack, newName tokens.QName) (StackRefer } // PreviewStack previews changes to this stack. -func PreviewStack(ctx context.Context, s Stack, op UpdateOperation) (engine.ResourceChanges, result.Result) { +func PreviewStack(ctx context.Context, s Stack, op UpdateOperation) (engine.Plan, engine.ResourceChanges, result.Result) { return s.Backend().Preview(ctx, s, op) } diff --git a/pkg/backend/watch.go b/pkg/backend/watch.go index 6e7a01e87..812e54c5d 100644 --- a/pkg/backend/watch.go +++ b/pkg/backend/watch.go @@ -96,7 +96,7 @@ func Watch(ctx context.Context, b Backend, stack Stack, op UpdateOperation, op.Opts.Display.Color.Colorize(colors.SpecImportant+"Updating..."+colors.Reset+"\n")) // Perform the update operation - _, res := apply(ctx, apitype.UpdateUpdate, stack, op, opts, nil) + _, _, res := apply(ctx, apitype.UpdateUpdate, stack, op, opts, nil) if res != nil { logging.V(5).Infof("watch update failed: %v", res.Error()) if res.Error() == context.Canceled { diff --git a/pkg/cmd/pulumi/preview.go b/pkg/cmd/pulumi/preview.go index 3084e03f9..2d2ac87db 100644 --- a/pkg/cmd/pulumi/preview.go +++ b/pkg/cmd/pulumi/preview.go @@ -36,6 +36,8 @@ func newPreviewCmd() *cobra.Command { var configArray []string var configPath bool var client string + var planFilePath string + var showSecrets bool // Flags for engine.UpdateOptions. var jsonDisplay bool @@ -182,7 +184,7 @@ func newPreviewCmd() *cobra.Command { Display: displayOpts, } - changes, res := s.Preview(commandContext(), backend.UpdateOperation{ + plan, changes, res := s.Preview(commandContext(), backend.UpdateOperation{ Proj: proj, Root: root, M: m, @@ -198,6 +200,16 @@ func newPreviewCmd() *cobra.Command { case expectNop && changes != nil && changes.HasChanges(): return result.FromError(errors.New("error: no changes were expected but changes were proposed")) default: + if planFilePath != "" { + encrypter, err := sm.Encrypter() + if err != nil { + return result.FromError(err) + } + if err = writePlan(planFilePath, plan, encrypter, showSecrets); err != nil { + return result.FromError(err) + } + // TODO(pdg-plan): emit a message about how to apply the plan + } return nil } }), @@ -221,6 +233,11 @@ func newPreviewCmd() *cobra.Command { cmd.PersistentFlags().BoolVar( &configPath, "config-path", false, "Config keys contain a path to a property in a map or list to set") + cmd.PersistentFlags().StringVar( + &planFilePath, "plan", "", + "Save the operations proposed by the preview to a plan file at the given path") + cmd.Flags().BoolVarP( + &showSecrets, "show-secrets", "", false, "Emit secrets in plaintext in the plan file. Defaults to `false`") cmd.PersistentFlags().StringVar( &client, "client", "", "The address of an existing language runtime host to connect to") diff --git a/pkg/cmd/pulumi/up.go b/pkg/cmd/pulumi/up.go index 304923f49..eab61acdf 100644 --- a/pkg/cmd/pulumi/up.go +++ b/pkg/cmd/pulumi/up.go @@ -76,6 +76,7 @@ func newUpCmd() *cobra.Command { var replaces []string var targetReplaces []string var targetDependents bool + var planFilePath string // up implementation used when the source of the Pulumi program is in the current working directory. upWorkingDirectory := func(opts backend.UpdateOptions) result.Result { @@ -143,6 +144,22 @@ func newUpCmd() *cobra.Command { TargetDependents: targetDependents, } + if planFilePath != "" { + dec, err := sm.Decrypter() + if err != nil { + return result.FromError(err) + } + enc, err := sm.Encrypter() + if err != nil { + return result.FromError(err) + } + plan, err := readPlan(planFilePath, dec, enc) + if err != nil { + return result.FromError(err) + } + opts.Engine.Plan = plan + } + changes, res := s.Update(commandContext(), backend.UpdateOperation{ Proj: proj, Root: root, @@ -505,6 +522,12 @@ func newUpCmd() *cobra.Command { &yes, "yes", "y", false, "Automatically approve and perform the update after previewing it") + cmd.PersistentFlags().StringVar( + &planFilePath, "plan", "", + "Path to a plan file to use for the update. The update will use property values from the plan, and will not "+ + "perform operations that exceed its constraints (e.g. replacements instead of updates, or updates instead"+ + "of sames).") + if hasDebugCommands() { cmd.PersistentFlags().StringVar( &eventLogPath, "event-log", "", diff --git a/pkg/cmd/pulumi/util.go b/pkg/cmd/pulumi/util.go index e05a81440..b1a202a16 100644 --- a/pkg/cmd/pulumi/util.go +++ b/pkg/cmd/pulumi/util.go @@ -45,6 +45,7 @@ import ( "github.com/pulumi/pulumi/pkg/v3/secrets/passphrase" "github.com/pulumi/pulumi/pkg/v3/util/cancel" "github.com/pulumi/pulumi/pkg/v3/util/tracing" + "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" "github.com/pulumi/pulumi/sdk/v3/go/common/constant" "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v3/go/common/util/ciutil" @@ -862,3 +863,31 @@ func getRefreshOption(proj *workspace.Project, refresh string) (bool, error) { // the default functionality right now is to always skip a refresh return false, nil } + +func writePlan(path string, plan engine.Plan, enc config.Encrypter, showSecrets bool) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer contract.IgnoreClose(f) + + deploymentPlan, err := stack.SerializePlan(plan, enc, showSecrets) + if err != nil { + return err + } + return json.NewEncoder(f).Encode(deploymentPlan) +} + +func readPlan(path string, dec config.Decrypter, enc config.Encrypter) (engine.Plan, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer contract.IgnoreClose(f) + + var deploymentPlan apitype.DeploymentPlanV1 + if err := json.NewDecoder(f).Decode(&deploymentPlan); err != nil { + return nil, err + } + return stack.DeserializePlan(deploymentPlan, dec, enc) +} diff --git a/pkg/resource/deploy/step_generator.go b/pkg/resource/deploy/step_generator.go index 3692a4231..04380ee19 100644 --- a/pkg/resource/deploy/step_generator.go +++ b/pkg/resource/deploy/step_generator.go @@ -171,6 +171,8 @@ func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, res resourcePlan, ok := sg.plan.newResourcePlans[s.URN()] if !ok { + // TODO(pdg-plan): using the program inputs means that non-determinism could sneak in as part of default + // application. However, it is necessary in the face of computed inputs. resourcePlan = &ResourcePlan{Goal: event.Goal()} sg.plan.newResourcePlans[s.URN()] = resourcePlan } @@ -272,6 +274,8 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res // If there is a plan for this resource, finalize its inputs. if resourcePlan, ok := sg.plan.resourcePlans[urn]; ok { + // should really overwrite goal info completely here + inputs = resourcePlan.completeInputs(inputs) } diff --git a/pkg/resource/stack/plan.go b/pkg/resource/stack/plan.go new file mode 100644 index 000000000..730c473f3 --- /dev/null +++ b/pkg/resource/stack/plan.go @@ -0,0 +1,102 @@ +package stack + +import ( + "github.com/pulumi/pulumi/pkg/v2/resource/deploy" + "github.com/pulumi/pulumi/sdk/v2/go/common/apitype" + "github.com/pulumi/pulumi/sdk/v2/go/common/resource" + "github.com/pulumi/pulumi/sdk/v2/go/common/resource/config" +) + +func SerializeResourcePlan(plan *deploy.ResourcePlan, enc config.Encrypter, showSecrets bool) (apitype.ResourcePlanV1, error) { + properties, err := SerializeProperties(plan.Goal.Properties, enc, showSecrets) + if err != nil { + return apitype.ResourcePlanV1{}, err + } + + goal := apitype.GoalV1{ + Type: plan.Goal.Type, + Name: plan.Goal.Name, + Custom: plan.Goal.Custom, + Properties: properties, + Parent: plan.Goal.Parent, + Protect: plan.Goal.Protect, + Dependencies: plan.Goal.Dependencies, + Provider: plan.Goal.Provider, + PropertyDependencies: plan.Goal.PropertyDependencies, + DeleteBeforeReplace: plan.Goal.DeleteBeforeReplace, + IgnoreChanges: plan.Goal.IgnoreChanges, + AdditionalSecretOutputs: plan.Goal.AdditionalSecretOutputs, + Aliases: plan.Goal.Aliases, + ID: plan.Goal.ID, + CustomTimeouts: plan.Goal.CustomTimeouts, + } + + steps := make([]apitype.OpType, len(plan.Ops)) + for i, op := range plan.Ops { + steps[i] = apitype.OpType(op) + } + + return apitype.ResourcePlanV1{ + Goal: goal, + Steps: steps, + }, nil +} + +func SerializePlan(plan map[resource.URN]*deploy.ResourcePlan, enc config.Encrypter, showSecrets bool) (apitype.DeploymentPlanV1, error) { + resourcePlans := map[resource.URN]apitype.ResourcePlanV1{} + for urn, plan := range plan { + serializedPlan, err := SerializeResourcePlan(plan, enc, showSecrets) + if err != nil { + return apitype.DeploymentPlanV1{}, err + } + resourcePlans[urn] = serializedPlan + } + return apitype.DeploymentPlanV1{ResourcePlans: resourcePlans}, nil +} + +func DeserializeResourcePlan(plan apitype.ResourcePlanV1, dec config.Decrypter, enc config.Encrypter) (*deploy.ResourcePlan, error) { + properties, err := DeserializeProperties(plan.Goal.Properties, dec, enc) + if err != nil { + return nil, err + } + + goal := &resource.Goal{ + Type: plan.Goal.Type, + Name: plan.Goal.Name, + Custom: plan.Goal.Custom, + Properties: properties, + Parent: plan.Goal.Parent, + Protect: plan.Goal.Protect, + Dependencies: plan.Goal.Dependencies, + Provider: plan.Goal.Provider, + PropertyDependencies: plan.Goal.PropertyDependencies, + DeleteBeforeReplace: plan.Goal.DeleteBeforeReplace, + IgnoreChanges: plan.Goal.IgnoreChanges, + AdditionalSecretOutputs: plan.Goal.AdditionalSecretOutputs, + Aliases: plan.Goal.Aliases, + ID: plan.Goal.ID, + CustomTimeouts: plan.Goal.CustomTimeouts, + } + + ops := make([]deploy.StepOp, len(plan.Steps)) + for i, op := range plan.Steps { + ops[i] = deploy.StepOp(op) + } + + return &deploy.ResourcePlan{ + Goal: goal, + Ops: ops, + }, nil +} + +func DeserializePlan(plan apitype.DeploymentPlanV1, dec config.Decrypter, enc config.Encrypter) (map[resource.URN]*deploy.ResourcePlan, error) { + resourcePlans := map[resource.URN]*deploy.ResourcePlan{} + for urn, plan := range plan.ResourcePlans { + deserializedPlan, err := DeserializeResourcePlan(plan, dec, enc) + if err != nil { + return nil, err + } + resourcePlans[urn] = deserializedPlan + } + return resourcePlans, nil +} diff --git a/sdk/go/common/apitype/plan.go b/sdk/go/common/apitype/plan.go new file mode 100644 index 000000000..101553aa4 --- /dev/null +++ b/sdk/go/common/apitype/plan.go @@ -0,0 +1,56 @@ +package apitype + +import ( + "github.com/pulumi/pulumi/sdk/v2/go/common/resource" + "github.com/pulumi/pulumi/sdk/v2/go/common/tokens" +) + +// GoalV1 is the serializable version of a resource goal state. +type GoalV1 struct { + // the type of resource. + Type tokens.Type `json:"type"` + // the name for the resource's URN. + Name tokens.QName `json:"name"` + // true if this resource is custom, managed by a plugin. + Custom bool `json:"custom"` + // the resource's input properties. + Properties map[string]interface{} `json:"properties,omitempty"` + // an optional parent URN for this resource. + Parent resource.URN `json:"parent,omitempty"` + // true to protect this resource from deletion. + Protect bool `json:"protect"` + // dependencies of this resource object. + Dependencies []resource.URN `json:"dependencies,omitempty"` + // the provider to use for this resource. + Provider string `json:"provider,omitempty"` + // the set of dependencies that affect each property. + PropertyDependencies map[resource.PropertyKey][]resource.URN `json:"propertyDependencies,omitempty"` + // true if this resource should be deleted prior to replacement. + DeleteBeforeReplace *bool `json:"deleteBeforeReplace,omitempty"` + // a list of property names to ignore during changes. + IgnoreChanges []string `json:"ignoreChanges,omitempty"` + // outputs that should always be treated as secrets. + AdditionalSecretOutputs []resource.PropertyKey `json:"additionalSecretOutputs,omitempty"` + // additional URNs that should be aliased to this resource. + Aliases []resource.URN `json:"aliases,omitempty"` + // the expected ID of the resource, if any. + ID resource.ID `json:"id,omitempty"` + // an optional config object for resource options + CustomTimeouts resource.CustomTimeouts `json:"customTimeouts,omitempty"` +} + +// ResourcePlanV1 is the serializable version of a resource plan. +type ResourcePlanV1 struct { + // The goal state for the resource. + Goal GoalV1 `json:"goal"` + // The steps to be performed on the resource. + Steps []OpType `json:"steps,omitempty"` +} + +// DeploymentPlanV1 is the serializable version of a deployment plan. +type DeploymentPlanV1 struct { + // TODO(pdg-plan): should there be a message here? + + // The set of resource plans. + ResourcePlans map[resource.URN]ResourcePlanV1 `json:"resourcePlans,omitempty"` +}