Plumb plans through the CLI.

This commit is contained in:
Pat Gavlin 2020-11-16 10:41:31 -08:00
parent 33e8980cee
commit e101f80839
15 changed files with 279 additions and 42 deletions

View file

@ -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 {

View file

@ -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.

View file

@ -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.

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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")

View file

@ -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", "",

View file

@ -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)
}

View file

@ -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)
}

102
pkg/resource/stack/plan.go Normal file
View file

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

View file

@ -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"`
}