Compare commits
80 commits
master
...
fraser/pla
Author | SHA1 | Date | |
---|---|---|---|
36bf0a43dd | |||
19dc1bf865 | |||
3bad3c4abf | |||
f0ad8cffc7 | |||
69d81638f9 | |||
605bc2ecdf | |||
884d460c39 | |||
429c93def9 | |||
a88200c70d | |||
146680abda | |||
6c337ad7f9 | |||
9242b835f4 | |||
f9bda75d38 | |||
1cf5e7b763 | |||
50232e26ab | |||
dfef30f5a6 | |||
bd7d9b797b | |||
8f959a03b0 | |||
94bf0840df | |||
37b22b8d3c | |||
6022b98134 | |||
202c0f0ae4 | |||
76f3e938aa | |||
b692140160 | |||
5f57c2ab72 | |||
ad6047cd08 | |||
b7a5310e07 | |||
e182cf0ff3 | |||
1ddd452faa | |||
7143b728b7 | |||
6d02c4adc8 | |||
b3d8cd9870 | |||
9032552d69 | |||
c1047fb3d9 | |||
9739ff0eed | |||
4fa395b03d | |||
598f2c7213 | |||
96af4684db | |||
844e8f0c1e | |||
20eaa27b3d | |||
de3e95b7ab | |||
23e9ec2c54 | |||
3b9cf5b648 | |||
40400b664b | |||
878c2bb28a | |||
83e655e19f | |||
b1a64a65ca | |||
5b6a03f548 | |||
e2c5f12d65 | |||
feaccb1fe1 | |||
9865fa58e3 | |||
c41981a84f | |||
1db1ab5137 | |||
82222cc19e | |||
775ace8f13 | |||
12c0188329 | |||
3e73c036fb | |||
f4c28053f8 | |||
d995901fa7 | |||
505e4c5d06 | |||
5352638e51 | |||
fdea5bb588 | |||
b5eb955301 | |||
b1e23a5287 | |||
83e2cd7e4a | |||
8fae881a81 | |||
1b59952a20 | |||
155c035e34 | |||
0d9043f6a2 | |||
90b75141cb | |||
365bfd2ef8 | |||
954bdc025b | |||
24a0eb0274 | |||
908c3aeb70 | |||
6de090c540 | |||
2818c9888d | |||
bbfd8256de | |||
13f8d342ce | |||
e101f80839 | |||
33e8980cee |
|
@ -26,6 +26,7 @@ import (
|
|||
|
||||
"github.com/pulumi/pulumi/pkg/v3/backend/display"
|
||||
"github.com/pulumi/pulumi/pkg/v3/engine"
|
||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
|
@ -44,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) (*deploy.Plan, engine.ResourceChanges, result.Result)
|
||||
|
||||
func ActionLabel(kind apitype.UpdateKind, dryRun bool) string {
|
||||
v := updateTextMap[kind]
|
||||
|
@ -79,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) (*deploy.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
|
||||
|
||||
|
@ -111,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.
|
||||
|
@ -196,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.
|
||||
|
@ -208,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 {
|
||||
|
|
|
@ -161,7 +161,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) (*deploy.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.
|
||||
|
|
|
@ -461,12 +461,12 @@ func (b *localBackend) PackPolicies(
|
|||
}
|
||||
|
||||
func (b *localBackend) Preview(ctx context.Context, stack backend.Stack,
|
||||
op backend.UpdateOperation) (engine.ResourceChanges, result.Result) {
|
||||
op backend.UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
|
||||
if cmdutil.IsTruthy(os.Getenv(PulumiFilestateLockingEnvVar)) {
|
||||
err := b.Lock(ctx, stack.Ref())
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer b.Unlock(ctx, stack.Ref())
|
||||
}
|
||||
|
@ -548,7 +548,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) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
|
||||
stackRef := stack.Ref()
|
||||
stackName := stackRef.Name()
|
||||
|
@ -563,7 +563,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.
|
||||
|
@ -604,19 +604,20 @@ func (b *localBackend) apply(
|
|||
|
||||
// Perform the update
|
||||
start := time.Now().Unix()
|
||||
var plan *deploy.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)
|
||||
}
|
||||
|
@ -660,16 +661,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(fmt.Errorf("saving update info: %w", saveErr))
|
||||
return plan, changes, result.FromError(fmt.Errorf("saving update info: %w", saveErr))
|
||||
}
|
||||
|
||||
if backupErr != nil {
|
||||
return changes, result.FromError(fmt.Errorf("saving backup: %w", backupErr))
|
||||
return plan, changes, result.FromError(fmt.Errorf("saving backup: %w", backupErr))
|
||||
}
|
||||
|
||||
// Make sure to print a link to the stack's checkpoint before exiting.
|
||||
|
@ -705,7 +706,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.
|
||||
|
|
|
@ -64,7 +64,10 @@ 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) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
|
||||
return backend.PreviewStack(ctx, s, op)
|
||||
}
|
||||
|
||||
|
|
|
@ -828,7 +828,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) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
// We can skip PreviewtThenPromptThenExecute, and just go straight to Execute.
|
||||
opts := backend.ApplierOptions{
|
||||
DryRun: true,
|
||||
|
@ -931,7 +931,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) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
|
||||
actionLabel := backend.ActionLabel(kind, opts.DryRun)
|
||||
|
||||
|
@ -945,7 +945,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 {
|
||||
|
@ -985,12 +985,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) (*deploy.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
|
||||
|
@ -1039,19 +1039,20 @@ func (b *cloudBackend) runEngineAction(
|
|||
engineCtx.ParentSpan = parentSpan.Context()
|
||||
}
|
||||
|
||||
var plan *deploy.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)
|
||||
}
|
||||
|
@ -1077,7 +1078,7 @@ func (b *cloudBackend) runEngineAction(
|
|||
res = result.Merge(res, result.FromError(fmt.Errorf("failed to complete update: %w", completeErr)))
|
||||
}
|
||||
|
||||
return changes, res
|
||||
return plan, changes, res
|
||||
}
|
||||
|
||||
func (b *cloudBackend) CancelCurrentUpdate(ctx context.Context, stackRef backend.StackReference) error {
|
||||
|
|
|
@ -140,7 +140,10 @@ 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) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
|
||||
return backend.PreviewStack(ctx, s, op)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) (*deploy.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) (*deploy.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) (*deploy.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,10 @@ 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) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
|
||||
if ms.PreviewF != nil {
|
||||
return ms.PreviewF(ctx, op)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,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) (*deploy.Plan, engine.ResourceChanges, result.Result)
|
||||
// Update this stack.
|
||||
Update(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
|
||||
// Import resources into this stack.
|
||||
|
@ -73,7 +73,11 @@ 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) (*deploy.Plan, engine.ResourceChanges, result.Result) {
|
||||
|
||||
return s.Backend().Preview(ctx, s, op)
|
||||
}
|
||||
|
||||
|
@ -117,7 +121,10 @@ func GetStackLogs(ctx context.Context, s Stack, cfg StackConfiguration,
|
|||
}
|
||||
|
||||
// ExportStackDeployment exports the given stack's deployment as an opaque JSON message.
|
||||
func ExportStackDeployment(ctx context.Context, s Stack) (*apitype.UntypedDeployment, error) {
|
||||
func ExportStackDeployment(
|
||||
ctx context.Context,
|
||||
s Stack) (*apitype.UntypedDeployment, error) {
|
||||
|
||||
return s.Backend().ExportDeployment(ctx, s)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -38,6 +38,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
|
||||
|
@ -184,7 +186,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,
|
||||
|
@ -200,6 +202,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
|
||||
}
|
||||
}),
|
||||
|
@ -223,6 +235,13 @@ 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")
|
||||
if hasExperimentalCommands() {
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&planFilePath, "save-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")
|
||||
|
|
|
@ -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,14 @@ func newUpCmd() *cobra.Command {
|
|||
&yes, "yes", "y", false,
|
||||
"Automatically approve and perform the update after previewing it")
|
||||
|
||||
if hasExperimentalCommands() {
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&planFilePath, "plan", "",
|
||||
"Path to a plan file to use for the update. The update will not "+
|
||||
"perform operations that exceed its plan (e.g. replacements instead of updates, or updates instead"+
|
||||
"of sames).")
|
||||
}
|
||||
|
||||
if hasDebugCommands() {
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&eventLogPath, "event-log", "",
|
||||
|
|
|
@ -42,12 +42,15 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/v3/backend/httpstate"
|
||||
"github.com/pulumi/pulumi/pkg/v3/backend/state"
|
||||
"github.com/pulumi/pulumi/pkg/v3/engine"
|
||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
||||
"github.com/pulumi/pulumi/pkg/v3/resource/stack"
|
||||
"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/resource/config"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/ciutil"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
||||
|
@ -866,6 +869,34 @@ func getRefreshOption(proj *workspace.Project, refresh string) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func writePlan(path string, plan *deploy.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) (*deploy.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)
|
||||
}
|
||||
|
||||
func buildStackName(stackName string) (string, error) {
|
||||
if strings.Count(stackName, "/") == 2 {
|
||||
return stackName, nil
|
||||
|
|
|
@ -167,7 +167,7 @@ func newDeployment(ctx *Context, info *deploymentContext, opts deploymentOptions
|
|||
var depl *deploy.Deployment
|
||||
if !opts.isImport {
|
||||
depl, err = deploy.NewDeployment(
|
||||
plugctx, target, target.Snapshot, source, localPolicyPackPaths, dryRun, ctx.BackendClient)
|
||||
plugctx, target, target.Snapshot, opts.Plan, source, localPolicyPackPaths, dryRun, ctx.BackendClient)
|
||||
} else {
|
||||
_, defaultProviderVersions, pluginErr := installPlugins(proj, pwd, main, target, plugctx,
|
||||
false /*returnInstallErrors*/)
|
||||
|
@ -218,12 +218,12 @@ type runActions interface {
|
|||
|
||||
// run executes the deployment. It is primarily responsible for handling cancellation.
|
||||
func (deployment *deployment) run(cancelCtx *Context, actions runActions, policyPacks map[string]string,
|
||||
preview bool) (ResourceChanges, result.Result) {
|
||||
preview bool) (*deploy.Plan, ResourceChanges, result.Result) {
|
||||
|
||||
// Change into the plugin context's working directory.
|
||||
chdir, err := fsutil.Chdir(deployment.Plugctx.Pwd)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer chdir()
|
||||
|
||||
|
@ -242,6 +242,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
|
|||
start := time.Now()
|
||||
|
||||
done := make(chan bool)
|
||||
var newPlan *deploy.Plan
|
||||
var walkResult result.Result
|
||||
go func() {
|
||||
opts := deploy.Options{
|
||||
|
@ -259,7 +260,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
|
|||
DisableResourceReferences: deployment.Options.DisableResourceReferences,
|
||||
DisableOutputValues: deployment.Options.DisableOutputValues,
|
||||
}
|
||||
walkResult = deployment.Deployment.Execute(ctx, opts, preview)
|
||||
newPlan, walkResult = deployment.Deployment.Execute(ctx, opts, preview)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
|
@ -290,7 +291,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
|
|||
// Emit a summary event.
|
||||
deployment.Options.Events.summaryEvent(preview, actions.MaybeCorrupt(), duration, changes, policyPacks)
|
||||
|
||||
return changes, res
|
||||
return newPlan, changes, res
|
||||
}
|
||||
|
||||
func (deployment *deployment) Close() error {
|
||||
|
|
|
@ -23,7 +23,12 @@ import (
|
|||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
||||
)
|
||||
|
||||
func Destroy(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
|
||||
func Destroy(
|
||||
u UpdateInfo,
|
||||
ctx *Context,
|
||||
opts UpdateOptions,
|
||||
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
|
||||
|
||||
contract.Require(u != nil, "u")
|
||||
contract.Require(ctx != nil, "ctx")
|
||||
|
||||
|
@ -31,13 +36,13 @@ func Destroy(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resou
|
|||
|
||||
info, err := newDeploymentContext(u, "destroy", ctx.ParentSpan)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer info.Close()
|
||||
|
||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer emitter.Close()
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
)
|
||||
|
||||
func Import(u UpdateInfo, ctx *Context, opts UpdateOptions, imports []deploy.Import,
|
||||
dryRun bool) (ResourceChanges, result.Result) {
|
||||
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
|
||||
|
||||
contract.Require(u != nil, "u")
|
||||
contract.Require(ctx != nil, "ctx")
|
||||
|
@ -30,13 +30,13 @@ func Import(u UpdateInfo, ctx *Context, opts UpdateOptions, imports []deploy.Imp
|
|||
|
||||
info, err := newDeploymentContext(u, "import", ctx.ParentSpan)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer info.Close()
|
||||
|
||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer emitter.Close()
|
||||
|
||||
|
|
|
@ -84,12 +84,12 @@ func TestImportOption(t *testing.T) {
|
|||
// Run the initial update. The import should fail due to a mismatch in inputs between the program and the
|
||||
// actual resource state.
|
||||
project := p.GetProject()
|
||||
_, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
||||
_, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
||||
assert.NotNil(t, res)
|
||||
|
||||
// Run a second update after fixing the inputs. The import should succeed.
|
||||
inputs["foo"] = resource.NewStringProperty("bar")
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -107,7 +107,7 @@ func TestImportOption(t *testing.T) {
|
|||
assert.Len(t, snap.Resources, 2)
|
||||
|
||||
// Now, run another update. The update should succeed and there should be no diffs.
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -123,7 +123,7 @@ func TestImportOption(t *testing.T) {
|
|||
|
||||
// Change a property value and run a third update. The update should succeed.
|
||||
inputs["foo"] = resource.NewStringProperty("rab")
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -141,11 +141,11 @@ func TestImportOption(t *testing.T) {
|
|||
|
||||
// Change the property value s.t. the resource requires replacement. The update should fail.
|
||||
inputs["foo"] = resource.NewStringProperty("replace")
|
||||
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient, nil)
|
||||
_, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)
|
||||
assert.NotNil(t, res)
|
||||
|
||||
// Finally, destroy the stack. The `Delete` function should be called.
|
||||
_, res = TestOp(Destroy).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
|
||||
_, res = TestOp(Destroy).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -161,7 +161,7 @@ func TestImportOption(t *testing.T) {
|
|||
|
||||
// Now clear the ID to import and run an initial update to create a resource that we will import-replace.
|
||||
importID, inputs["foo"] = "", resource.NewStringProperty("bar")
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -182,7 +182,7 @@ func TestImportOption(t *testing.T) {
|
|||
importID = r.ID
|
||||
}
|
||||
}
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -199,7 +199,7 @@ func TestImportOption(t *testing.T) {
|
|||
// Then set the import ID and run another update. The update should succeed and should show an import-replace and
|
||||
// a delete-replaced.
|
||||
importID = "id"
|
||||
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
|
||||
_, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -222,7 +222,7 @@ func TestImportOption(t *testing.T) {
|
|||
|
||||
// Change the program to read a resource rather than creating one.
|
||||
readID = "id"
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -241,7 +241,7 @@ func TestImportOption(t *testing.T) {
|
|||
|
||||
// Now have the program import the resource. We should see an import-replace and a read-discard.
|
||||
readID, importID = "", readID
|
||||
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
|
||||
_, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -327,7 +327,7 @@ func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
|
|||
|
||||
// Run the initial update. The import should succeed.
|
||||
project := p.GetProject()
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -345,7 +345,7 @@ func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
|
|||
assert.Len(t, snap.Resources, 2)
|
||||
|
||||
// Now, run another update. The update should succeed and there should be no diffs.
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
|
||||
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
|
||||
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||
for _, entry := range entries {
|
||||
switch urn := entry.Step.URN(); urn {
|
||||
|
@ -483,7 +483,7 @@ func TestImportPlan(t *testing.T) {
|
|||
|
||||
// Run the initial update.
|
||||
project := p.GetProject()
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// Run an import.
|
||||
|
@ -491,7 +491,7 @@ func TestImportPlan(t *testing.T) {
|
|||
Type: "pkgA:m:typA",
|
||||
Name: "resB",
|
||||
ID: "imported-id",
|
||||
}}).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient, nil)
|
||||
}}).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)
|
||||
|
||||
assert.Nil(t, res)
|
||||
assert.Len(t, snap.Resources, 4)
|
||||
|
@ -552,7 +552,7 @@ func TestImportIgnoreChanges(t *testing.T) {
|
|||
}
|
||||
|
||||
project := p.GetProject()
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
||||
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
||||
assert.Nil(t, res)
|
||||
|
||||
assert.Len(t, snap.Resources, 2)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -678,7 +678,7 @@ func TestCanceledRefresh(t *testing.T) {
|
|||
Parallel: 1,
|
||||
Host: deploytest.NewPluginHost(nil, nil, nil, loaders...),
|
||||
}
|
||||
project, target := p.GetProject(), p.GetTarget(old)
|
||||
project, target := p.GetProject(), p.GetTarget(t, old)
|
||||
validate := func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
||||
_ []Event, res result.Result) result.Result {
|
||||
|
||||
|
|
|
@ -39,16 +39,25 @@ func (u *updateInfo) GetTarget() *deploy.Target {
|
|||
}
|
||||
|
||||
func ImportOp(imports []deploy.Import) TestOp {
|
||||
return TestOp(func(info UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
|
||||
return TestOp(func(info UpdateInfo, ctx *Context, opts UpdateOptions,
|
||||
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
|
||||
|
||||
return Import(info, ctx, opts, imports, dryRun)
|
||||
})
|
||||
}
|
||||
|
||||
type TestOp func(UpdateInfo, *Context, UpdateOptions, bool) (ResourceChanges, result.Result)
|
||||
type TestOp func(UpdateInfo, *Context, UpdateOptions, bool) (*deploy.Plan, ResourceChanges, result.Result)
|
||||
|
||||
type ValidateFunc func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
||||
events []Event, res result.Result) result.Result
|
||||
|
||||
func (op TestOp) Plan(project workspace.Project, target deploy.Target, opts UpdateOptions,
|
||||
backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Plan, result.Result) {
|
||||
|
||||
plan, _, res := op.runWithContext(context.Background(), project, target, opts, true, backendClient, validate)
|
||||
return plan, res
|
||||
}
|
||||
|
||||
func (op TestOp) Run(project workspace.Project, target deploy.Target, opts UpdateOptions,
|
||||
dryRun bool, backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
|
||||
|
||||
|
@ -60,6 +69,15 @@ func (op TestOp) RunWithContext(
|
|||
target deploy.Target, opts UpdateOptions, dryRun bool,
|
||||
backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
|
||||
|
||||
_, snap, res := op.runWithContext(callerCtx, project, target, opts, dryRun, backendClient, validate)
|
||||
return snap, res
|
||||
}
|
||||
|
||||
func (op TestOp) runWithContext(
|
||||
callerCtx context.Context, project workspace.Project,
|
||||
target deploy.Target, opts UpdateOptions, dryRun bool,
|
||||
backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Plan, *deploy.Snapshot, result.Result) {
|
||||
|
||||
// Create an appropriate update info and context.
|
||||
info := &updateInfo{project: project, target: target}
|
||||
|
||||
|
@ -93,21 +111,21 @@ func (op TestOp) RunWithContext(
|
|||
}()
|
||||
|
||||
// Run the step and its validator.
|
||||
_, res := op(info, ctx, opts, dryRun)
|
||||
plan, _, res := op(info, ctx, opts, dryRun)
|
||||
contract.IgnoreClose(journal)
|
||||
|
||||
if validate != nil {
|
||||
res = validate(project, target, journal.Entries(), firedEvents, res)
|
||||
}
|
||||
if dryRun {
|
||||
return nil, res
|
||||
return plan, nil, res
|
||||
}
|
||||
|
||||
snap := journal.Snap(target.Snapshot)
|
||||
if res == nil && snap != nil {
|
||||
res = result.WrapIfNonNil(snap.VerifyIntegrity())
|
||||
}
|
||||
return snap, res
|
||||
return nil, snap, res
|
||||
}
|
||||
|
||||
type TestStep struct {
|
||||
|
@ -168,7 +186,7 @@ func (p *TestPlan) GetProject() workspace.Project {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *TestPlan) GetTarget(snapshot *deploy.Snapshot) deploy.Target {
|
||||
func (p *TestPlan) GetTarget(t *testing.T, snapshot *deploy.Snapshot) deploy.Target {
|
||||
stack, _, _ := p.getNames()
|
||||
|
||||
cfg := p.Config
|
||||
|
@ -180,7 +198,10 @@ func (p *TestPlan) GetTarget(snapshot *deploy.Snapshot) deploy.Target {
|
|||
Name: stack,
|
||||
Config: cfg,
|
||||
Decrypter: p.Decrypter,
|
||||
Snapshot: snapshot,
|
||||
// note: it's really important that the preview and update operate on different snapshots. the engine can and
|
||||
// does mutate the snapshot in-place, even in previews, and sharing a snapshot between preview and update can
|
||||
// cause state changes from the preview to persist even when doing an update.
|
||||
Snapshot: CloneSnapshot(t, snapshot),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,6 +209,18 @@ func assertIsErrorOrBailResult(t *testing.T, res result.Result) {
|
|||
assert.NotNil(t, res)
|
||||
}
|
||||
|
||||
// ClonePlan makes a deep copy of the given plan and returns a pointer to the clone.
|
||||
func ClonePlan(t *testing.T, plan *deploy.Plan) *deploy.Plan {
|
||||
t.Helper()
|
||||
if plan != nil {
|
||||
copiedPlan := copystructure.Must(copystructure.Copy(plan)).(*deploy.Plan)
|
||||
assert.True(t, reflect.DeepEqual(plan, copiedPlan))
|
||||
return copiedPlan
|
||||
}
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
// CloneSnapshot makes a deep copy of the given snapshot and returns a pointer to the clone.
|
||||
func CloneSnapshot(t *testing.T, snap *deploy.Snapshot) *deploy.Snapshot {
|
||||
t.Helper()
|
||||
|
@ -204,12 +237,9 @@ func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot
|
|||
project := p.GetProject()
|
||||
snap := snapshot
|
||||
for _, step := range p.Steps {
|
||||
// note: it's really important that the preview and update operate on different snapshots. the engine can and
|
||||
// does mutate the snapshot in-place, even in previews, and sharing a snapshot between preview and update can
|
||||
// cause state changes from the preview to persist even when doing an update.
|
||||
if !step.SkipPreview {
|
||||
previewSnap := CloneSnapshot(t, snap)
|
||||
previewTarget := p.GetTarget(previewSnap)
|
||||
previewTarget := p.GetTarget(t, previewSnap)
|
||||
// Don't run validate on the preview step
|
||||
_, res := step.Op.Run(project, previewTarget, p.Options, true, p.BackendClient, nil)
|
||||
if step.ExpectFailure {
|
||||
|
@ -221,7 +251,7 @@ func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot
|
|||
}
|
||||
|
||||
var res result.Result
|
||||
target := p.GetTarget(snap)
|
||||
target := p.GetTarget(t, snap)
|
||||
snap, res = step.Op.Run(project, target, p.Options, false, p.BackendClient, step.Validate)
|
||||
if step.ExpectFailure {
|
||||
assertIsErrorOrBailResult(t, res)
|
||||
|
|
|
@ -23,7 +23,12 @@ import (
|
|||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
||||
)
|
||||
|
||||
func Refresh(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
|
||||
func Refresh(
|
||||
u UpdateInfo,
|
||||
ctx *Context,
|
||||
opts UpdateOptions,
|
||||
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
|
||||
|
||||
contract.Require(u != nil, "u")
|
||||
contract.Require(ctx != nil, "ctx")
|
||||
|
||||
|
@ -31,13 +36,13 @@ func Refresh(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resou
|
|||
|
||||
info, err := newDeploymentContext(u, "refresh", ctx.ParentSpan)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer info.Close()
|
||||
|
||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer emitter.Close()
|
||||
|
||||
|
|
|
@ -146,6 +146,9 @@ type UpdateOptions struct {
|
|||
|
||||
// the plugin host to use for this update
|
||||
Host plugin.Host
|
||||
|
||||
// The plan to use for the update, if any.
|
||||
Plan *deploy.Plan
|
||||
}
|
||||
|
||||
// ResourceChanges contains the aggregate resource changes by operation type.
|
||||
|
@ -165,7 +168,9 @@ func (changes ResourceChanges) HasChanges() bool {
|
|||
return c > 0
|
||||
}
|
||||
|
||||
func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
|
||||
func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (
|
||||
*deploy.Plan, ResourceChanges, result.Result) {
|
||||
|
||||
contract.Require(u != nil, "update")
|
||||
contract.Require(ctx != nil, "ctx")
|
||||
|
||||
|
@ -173,13 +178,13 @@ func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resour
|
|||
|
||||
info, err := newDeploymentContext(u, "update", ctx.ParentSpan)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer info.Close()
|
||||
|
||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer emitter.Close()
|
||||
|
||||
|
@ -420,7 +425,7 @@ func newUpdateSource(
|
|||
}
|
||||
|
||||
func update(ctx *Context, info *deploymentContext, opts deploymentOptions,
|
||||
preview bool) (ResourceChanges, result.Result) {
|
||||
preview bool) (*deploy.Plan, ResourceChanges, result.Result) {
|
||||
|
||||
// Refresh and Import do not execute Policy Packs.
|
||||
policies := map[string]string{}
|
||||
|
@ -445,7 +450,7 @@ func update(ctx *Context, info *deploymentContext, opts deploymentOptions,
|
|||
|
||||
deployment, err := newDeployment(ctx, info, opts, preview)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
return nil, nil, result.FromError(err)
|
||||
}
|
||||
defer contract.IgnoreClose(deployment)
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/v3/resource/graph"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
||||
|
@ -145,6 +146,36 @@ func (m *resourceMap) mapRange(callback func(urn resource.URN, state *resource.S
|
|||
})
|
||||
}
|
||||
|
||||
type resourcePlans struct {
|
||||
m sync.RWMutex
|
||||
plans Plan
|
||||
}
|
||||
|
||||
func newResourcePlan(config config.Map) *resourcePlans {
|
||||
return &resourcePlans{
|
||||
plans: NewPlan(config),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *resourcePlans) set(urn resource.URN, plan *ResourcePlan) {
|
||||
m.m.Lock()
|
||||
defer m.m.Unlock()
|
||||
|
||||
m.plans.ResourcePlans[urn] = plan
|
||||
}
|
||||
|
||||
func (m *resourcePlans) get(urn resource.URN) (*ResourcePlan, bool) {
|
||||
m.m.RLock()
|
||||
defer m.m.RUnlock()
|
||||
|
||||
p, ok := m.plans.ResourcePlans[urn]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func (m *resourcePlans) plan() *Plan {
|
||||
return &m.plans
|
||||
}
|
||||
|
||||
// A Deployment manages the iterative computation and execution of a deployment based on a stream of goal states.
|
||||
// A running deployment emits events that indicate its progress. These events must be used to record the new state
|
||||
// of the deployment target.
|
||||
|
@ -153,6 +184,7 @@ type Deployment struct {
|
|||
target *Target // the deployment target.
|
||||
prev *Snapshot // the old resource snapshot for comparison.
|
||||
olds map[resource.URN]*resource.State // a map of all old resources.
|
||||
plan *Plan // a map of all planned resource changes, if any.
|
||||
imports []Import // resources to import, if this is an import deployment.
|
||||
isImport bool // true if this is an import deployment.
|
||||
schemaLoader schema.Loader // the schema cache for this deployment, if any.
|
||||
|
@ -162,7 +194,8 @@ type Deployment struct {
|
|||
depGraph *graph.DependencyGraph // the dependency graph of the old snapshot.
|
||||
providers *providers.Registry // the provider registry for this deployment.
|
||||
goals *goalMap // the set of resource goals generated by the deployment.
|
||||
news *resourceMap // the set of new resources generated by the deployment.
|
||||
news *resourceMap // the set of new resources generated by the deployment
|
||||
newPlans *resourcePlans // the set of new resource plans.
|
||||
}
|
||||
|
||||
// addDefaultProviders adds any necessary default provider definitions and references to the given snapshot. Version
|
||||
|
@ -299,7 +332,7 @@ func buildResourceMap(prev *Snapshot, preview bool) ([]*resource.State, map[reso
|
|||
//
|
||||
// Note that a deployment uses internal concurrency and parallelism in various ways, so it must be closed if for some
|
||||
// reason it isn't carried out to its final conclusion. This will result in cancellation and reclamation of resources.
|
||||
func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source Source,
|
||||
func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, plan *Plan, source Source,
|
||||
localPolicyPackPaths []string, preview bool, backendClient BackendClient) (*Deployment, error) {
|
||||
|
||||
contract.Assert(ctx != nil)
|
||||
|
@ -343,6 +376,7 @@ func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source S
|
|||
ctx: ctx,
|
||||
target: target,
|
||||
prev: prev,
|
||||
plan: plan,
|
||||
olds: olds,
|
||||
source: source,
|
||||
localPolicyPackPaths: localPolicyPackPaths,
|
||||
|
@ -351,6 +385,7 @@ func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source S
|
|||
providers: reg,
|
||||
goals: newGoals,
|
||||
news: newResources,
|
||||
newPlans: newResourcePlan(target.Config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -405,7 +440,7 @@ func (d *Deployment) generateEventURN(event SourceEvent) resource.URN {
|
|||
}
|
||||
|
||||
// Execute executes a deployment to completion, using the given cancellation context and running a preview or update.
|
||||
func (d *Deployment) Execute(ctx context.Context, opts Options, preview bool) result.Result {
|
||||
func (d *Deployment) Execute(ctx context.Context, opts Options, preview bool) (*Plan, result.Result) {
|
||||
deploymentExec := &deploymentExecutor{deployment: d}
|
||||
return deploymentExec.Execute(ctx, opts, preview)
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ func (ex *deploymentExecutor) reportError(urn resource.URN, err error) {
|
|||
|
||||
// Execute executes a deployment to completion, using the given cancellation context and running a preview
|
||||
// or update.
|
||||
func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, preview bool) result.Result {
|
||||
func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, preview bool) (*Plan, result.Result) {
|
||||
// Set up a goroutine that will signal cancellation to the deployment's plugins if the caller context is cancelled.
|
||||
// We do not hang this off of the context we create below because we do not want the failure of a single step to
|
||||
// cause other steps to fail.
|
||||
|
@ -141,10 +141,10 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
// Before doing anything else, optionally refresh each resource in the base checkpoint.
|
||||
if opts.Refresh {
|
||||
if res := ex.refresh(callerCtx, opts, preview); res != nil {
|
||||
return res
|
||||
return nil, res
|
||||
}
|
||||
if opts.RefreshOnly {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,10 +156,10 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
replaceTargetsOpt := createTargetMap(opts.ReplaceTargets)
|
||||
destroyTargetsOpt := createTargetMap(opts.DestroyTargets)
|
||||
if res := ex.checkTargets(opts.ReplaceTargets, OpReplace); res != nil {
|
||||
return res
|
||||
return nil, res
|
||||
}
|
||||
if res := ex.checkTargets(opts.DestroyTargets, OpDelete); res != nil {
|
||||
return res
|
||||
return nil, res
|
||||
}
|
||||
|
||||
if (updateTargetsOpt != nil || replaceTargetsOpt != nil) && destroyTargetsOpt != nil {
|
||||
|
@ -169,7 +169,7 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
// Begin iterating the source.
|
||||
src, res := ex.deployment.source.Iterate(callerCtx, opts, ex.deployment)
|
||||
if res != nil {
|
||||
return res
|
||||
return nil, res
|
||||
}
|
||||
|
||||
// Set up a step generator for this deployment.
|
||||
|
@ -177,7 +177,7 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
|
||||
// Retire any pending deletes that are currently present in this deployment.
|
||||
if res := ex.retirePendingDeletes(callerCtx, opts, preview); res != nil {
|
||||
return res
|
||||
return nil, res
|
||||
}
|
||||
|
||||
// Derive a cancellable context for this deployment. We will only cancel this context if some piece of the
|
||||
|
@ -236,7 +236,15 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
}
|
||||
|
||||
if event.Event == nil {
|
||||
return false, ex.performDeletes(ctx, updateTargetsOpt, destroyTargetsOpt)
|
||||
res := ex.performDeletes(ctx, updateTargetsOpt, destroyTargetsOpt)
|
||||
if res != nil {
|
||||
if resErr := res.Error(); resErr != nil {
|
||||
logging.V(4).Infof("deploymentExecutor.Execute(...): error performing deletes: %v", resErr)
|
||||
ex.reportError("", resErr)
|
||||
return false, result.Bail()
|
||||
}
|
||||
}
|
||||
return false, res
|
||||
}
|
||||
|
||||
if res := ex.handleSingleEvent(event.Event); res != nil {
|
||||
|
@ -267,8 +275,38 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
res = ex.checkTargets(opts.UpdateTargets, OpUpdate)
|
||||
}
|
||||
|
||||
// Check that we did operations for everything expected in the plan. We mutate ResourcePlan.Ops as we run
|
||||
// so by the time we get here everything in the map should have an empty ops list (except for unneeded deletes)
|
||||
if res == nil && ex.deployment.plan != nil {
|
||||
for urn, resourcePlan := range ex.deployment.plan.ResourcePlans {
|
||||
if len(resourcePlan.Ops) != 0 {
|
||||
if len(resourcePlan.Ops) == 1 && resourcePlan.Ops[0] == OpDelete {
|
||||
// We haven't done a delete for this resource check if it was in the snapshot,
|
||||
// if it's already gone this wasn't done because it wasn't needed
|
||||
found := false
|
||||
for i := range ex.deployment.prev.Resources {
|
||||
if ex.deployment.prev.Resources[i].URN == urn {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Didn't find the resource in the old snapshot so this was just an unneeded delete
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err := fmt.Errorf("expected resource operations for %v but none were seen", urn)
|
||||
logging.V(4).Infof("deploymentExecutor.Execute(...): error handling event: %v", err)
|
||||
ex.reportError(urn, err)
|
||||
res = result.Bail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if res != nil && res.IsBail() {
|
||||
return res
|
||||
return nil, res
|
||||
}
|
||||
|
||||
// If the step generator and step executor were both successful, then we send all the resources
|
||||
|
@ -280,7 +318,7 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
logging.V(4).Infof("deploymentExecutor.Execute(...): error analyzing resources: %v", resErr)
|
||||
ex.reportError("", resErr)
|
||||
}
|
||||
return result.Bail()
|
||||
return nil, result.Bail()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,13 +327,13 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
|
|||
// TODO(cyrusn): We seem to be losing any information about the original 'res's errors. Should
|
||||
// we be doing a merge here?
|
||||
ex.reportExecResult("failed", preview)
|
||||
return result.Bail()
|
||||
return nil, result.Bail()
|
||||
} else if canceled {
|
||||
ex.reportExecResult("canceled", preview)
|
||||
return result.Bail()
|
||||
return nil, result.Bail()
|
||||
}
|
||||
|
||||
return res
|
||||
return ex.deployment.newPlans.plan(), res
|
||||
}
|
||||
|
||||
func (ex *deploymentExecutor) performDeletes(
|
||||
|
@ -375,8 +413,7 @@ func (ex *deploymentExecutor) handleSingleEvent(event SourceEvent) result.Result
|
|||
steps, res = ex.stepGen.GenerateReadSteps(e)
|
||||
case RegisterResourceOutputsEvent:
|
||||
logging.V(4).Infof("deploymentExecutor.handleSingleEvent(...): received register resource outputs")
|
||||
ex.stepExec.ExecuteRegisterResourceOutputs(e)
|
||||
return nil
|
||||
return ex.stepExec.ExecuteRegisterResourceOutputs(e)
|
||||
}
|
||||
|
||||
if res != nil {
|
||||
|
@ -433,9 +470,13 @@ func (ex *deploymentExecutor) retirePendingDeletes(callerCtx context.Context, op
|
|||
}
|
||||
|
||||
// import imports a list of resources into a stack.
|
||||
func (ex *deploymentExecutor) importResources(callerCtx context.Context, opts Options, preview bool) result.Result {
|
||||
func (ex *deploymentExecutor) importResources(
|
||||
callerCtx context.Context,
|
||||
opts Options,
|
||||
preview bool) (*Plan, result.Result) {
|
||||
|
||||
if len(ex.deployment.imports) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create an executor for this import.
|
||||
|
@ -461,12 +502,12 @@ func (ex *deploymentExecutor) importResources(callerCtx context.Context, opts Op
|
|||
} else {
|
||||
ex.reportExecResult("failed", preview)
|
||||
}
|
||||
return result.Bail()
|
||||
return nil, result.Bail()
|
||||
} else if canceled {
|
||||
ex.reportExecResult("canceled", preview)
|
||||
return result.Bail()
|
||||
return nil, result.Bail()
|
||||
}
|
||||
return nil
|
||||
return ex.deployment.newPlans.plan(), nil
|
||||
}
|
||||
|
||||
// refresh refreshes the state of the base checkpoint file for the current deployment in memory.
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestPendingOperationsDeployment(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
_, err := NewDeployment(&plugin.Context{}, &Target{}, snap, &fixedSource{}, nil, false, nil)
|
||||
_, err := NewDeployment(&plugin.Context{}, &Target{}, snap, nil, &fixedSource{}, nil, false, nil)
|
||||
if !assert.Error(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@ func NewImportDeployment(ctx *plugin.Context, target *Target, projectName tokens
|
|||
source: NewErrorSource(projectName),
|
||||
preview: preview,
|
||||
providers: reg,
|
||||
newPlans: newResourcePlan(target.Config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
89
pkg/resource/deploy/manifest.go
Normal file
89
pkg/resource/deploy/manifest.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
||||
)
|
||||
|
||||
// Manifest captures versions for all binaries used to construct this snapshot.
|
||||
type Manifest struct {
|
||||
Time time.Time // the time this snapshot was taken.
|
||||
Magic string // a magic cookie.
|
||||
Version string // the pulumi command version.
|
||||
Plugins []workspace.PluginInfo // the plugin versions also loaded.
|
||||
}
|
||||
|
||||
// Serialize turns a manifest into a data structure suitable for serialization.
|
||||
func (m Manifest) Serialize() apitype.ManifestV1 {
|
||||
manifest := apitype.ManifestV1{
|
||||
Time: m.Time,
|
||||
Magic: m.Magic,
|
||||
Version: m.Version,
|
||||
}
|
||||
for _, plug := range m.Plugins {
|
||||
var version string
|
||||
if plug.Version != nil {
|
||||
version = plug.Version.String()
|
||||
}
|
||||
manifest.Plugins = append(manifest.Plugins, apitype.PluginInfoV1{
|
||||
Name: plug.Name,
|
||||
Path: plug.Path,
|
||||
Type: plug.Kind,
|
||||
Version: version,
|
||||
})
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
|
||||
// DeserializeManifest deserializes a typed ManifestV1 into a `deploy.Manifest`.
|
||||
func DeserializeManifest(m apitype.ManifestV1) (*Manifest, error) {
|
||||
manifest := Manifest{
|
||||
Time: m.Time,
|
||||
Magic: m.Magic,
|
||||
Version: m.Version,
|
||||
}
|
||||
for _, plug := range m.Plugins {
|
||||
var version *semver.Version
|
||||
if v := plug.Version; v != "" {
|
||||
sv, err := semver.ParseTolerant(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version = &sv
|
||||
}
|
||||
manifest.Plugins = append(manifest.Plugins, workspace.PluginInfo{
|
||||
Name: plug.Name,
|
||||
Kind: plug.Type,
|
||||
Version: version,
|
||||
})
|
||||
}
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
// NewMagic creates a magic cookie out of a manifest; this can be used to check for tampering. This ignores
|
||||
// any existing magic value already stored on the manifest.
|
||||
func (m Manifest) NewMagic() string {
|
||||
if m.Version == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Version)))
|
||||
}
|
496
pkg/resource/deploy/plan.go
Normal file
496
pkg/resource/deploy/plan.go
Normal file
|
@ -0,0 +1,496 @@
|
|||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
||||
"github.com/pulumi/pulumi/pkg/v3/version"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
||||
)
|
||||
|
||||
// A Plan is a mapping from URNs to ResourcePlans. The plan defines an expected set of resources and the expected
|
||||
// inputs and operations for each. The inputs and operations are treated as constraints, and may allow for inputs or
|
||||
// operations that do not exactly match those recorded in the plan. In the case of inputs, unknown values in the plan
|
||||
// accept any value (including no value) as valid. For operations, a same step is allowed in place of an update or
|
||||
// a replace step, and an update is allowed in place of a replace step. All resource options are required to match
|
||||
// exactly.
|
||||
type Plan struct {
|
||||
ResourcePlans map[resource.URN]*ResourcePlan
|
||||
Manifest Manifest
|
||||
// Any environment variables that were set when the plan was created. Values are encrypted.
|
||||
EnvironmentVariables map[string][]byte
|
||||
// The configuration in use during the plan.
|
||||
Config config.Map
|
||||
}
|
||||
|
||||
func NewPlan(config config.Map) Plan {
|
||||
manifest := Manifest{
|
||||
Time: time.Now(),
|
||||
Version: version.Version,
|
||||
// Plugins: sm.plugins, - Explicitly dropped, since we don't use the plugin list in the manifest anymore.
|
||||
}
|
||||
manifest.Magic = manifest.NewMagic()
|
||||
|
||||
return Plan{
|
||||
ResourcePlans: make(map[resource.URN]*ResourcePlan),
|
||||
Manifest: manifest,
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Goal is a desired state for a resource object. Normally it represents a subset of the resource's state expressed by
|
||||
// a program, however if Output is true, it represents a more complete, post-deployment view of the state.
|
||||
type GoalPlan struct {
|
||||
// the type of resource.
|
||||
Type tokens.Type
|
||||
// the name for the resource's URN.
|
||||
Name tokens.QName
|
||||
// true if this resource is custom, managed by a plugin.
|
||||
Custom bool
|
||||
// the resource's properties we expect to add.
|
||||
Adds resource.PropertyMap
|
||||
// the resource's properties we expect to delete.
|
||||
Deletes []resource.PropertyKey
|
||||
// the resource's properties we expect to update.
|
||||
Updates resource.PropertyMap
|
||||
// an optional parent URN for this resource.
|
||||
Parent resource.URN
|
||||
// true to protect this resource from deletion.
|
||||
Protect bool
|
||||
// dependencies of this resource object.
|
||||
Dependencies []resource.URN
|
||||
// the provider to use for this resource.
|
||||
Provider string
|
||||
// the set of dependencies that affect each property.
|
||||
PropertyDependencies map[resource.PropertyKey][]resource.URN
|
||||
// true if this resource should be deleted prior to replacement.
|
||||
DeleteBeforeReplace *bool
|
||||
// a list of property names to ignore during changes.
|
||||
IgnoreChanges []string
|
||||
// outputs that should always be treated as secrets.
|
||||
AdditionalSecretOutputs []resource.PropertyKey
|
||||
// additional URNs that should be aliased to this resource.
|
||||
Aliases []resource.URN
|
||||
// the expected ID of the resource, if any.
|
||||
ID resource.ID
|
||||
// an optional config object for resource options
|
||||
CustomTimeouts resource.CustomTimeouts
|
||||
}
|
||||
|
||||
func NewGoalPlan(oldOutputs resource.PropertyMap, goal *resource.Goal) *GoalPlan {
|
||||
if goal == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var adds resource.PropertyMap
|
||||
var deletes []resource.PropertyKey
|
||||
var updates resource.PropertyMap
|
||||
|
||||
if diff, hasDiff := oldOutputs.DiffIncludeUnknowns(goal.Properties); hasDiff {
|
||||
adds = diff.Adds
|
||||
updates = make(resource.PropertyMap)
|
||||
for k := range diff.Updates {
|
||||
updates[k] = diff.Updates[k].New
|
||||
}
|
||||
deletes = make([]resource.PropertyKey, len(diff.Deletes))
|
||||
i := 0
|
||||
for k := range diff.Deletes {
|
||||
deletes[i] = k
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
return &GoalPlan{
|
||||
Type: goal.Type,
|
||||
Name: goal.Name,
|
||||
Custom: goal.Custom,
|
||||
Adds: adds,
|
||||
Deletes: deletes,
|
||||
Updates: updates,
|
||||
Parent: goal.Parent,
|
||||
Protect: goal.Protect,
|
||||
Dependencies: goal.Dependencies,
|
||||
Provider: goal.Provider,
|
||||
PropertyDependencies: goal.PropertyDependencies,
|
||||
DeleteBeforeReplace: goal.DeleteBeforeReplace,
|
||||
IgnoreChanges: goal.IgnoreChanges,
|
||||
AdditionalSecretOutputs: goal.AdditionalSecretOutputs,
|
||||
Aliases: goal.Aliases,
|
||||
ID: goal.ID,
|
||||
CustomTimeouts: goal.CustomTimeouts,
|
||||
}
|
||||
}
|
||||
|
||||
// A ResourcePlan represents the planned goal state and resource operations for a single resource. The operations are
|
||||
// ordered.
|
||||
type ResourcePlan struct {
|
||||
Goal *GoalPlan
|
||||
Ops []StepOp
|
||||
Outputs resource.PropertyMap
|
||||
}
|
||||
|
||||
func (rp *ResourcePlan) diffURNs(a, b []resource.URN) (message string, changed bool) {
|
||||
stringsA := make([]string, len(a))
|
||||
for i, urn := range a {
|
||||
stringsA[i] = string(urn)
|
||||
}
|
||||
stringsB := make([]string, len(a))
|
||||
for i, urn := range b {
|
||||
stringsB[i] = string(urn)
|
||||
}
|
||||
return rp.diffStrings(stringsA, stringsB)
|
||||
}
|
||||
|
||||
func (rp *ResourcePlan) diffPropertyKeys(a, b []resource.PropertyKey) (message string, changed bool) {
|
||||
stringsA := make([]string, len(a))
|
||||
for i, key := range a {
|
||||
stringsA[i] = string(key)
|
||||
}
|
||||
stringsB := make([]string, len(a))
|
||||
for i, key := range b {
|
||||
stringsB[i] = string(key)
|
||||
}
|
||||
return rp.diffStrings(stringsA, stringsB)
|
||||
}
|
||||
|
||||
func (rp *ResourcePlan) diffStrings(a, b []string) (message string, changed bool) {
|
||||
setA := map[string]struct{}{}
|
||||
for _, s := range a {
|
||||
setA[s] = struct{}{}
|
||||
}
|
||||
|
||||
setB := map[string]struct{}{}
|
||||
for _, s := range b {
|
||||
setB[s] = struct{}{}
|
||||
}
|
||||
|
||||
var adds, deletes []string
|
||||
for s := range setA {
|
||||
if _, has := setB[s]; !has {
|
||||
deletes = append(deletes, s)
|
||||
}
|
||||
}
|
||||
for s := range setB {
|
||||
if _, has := setA[s]; !has {
|
||||
adds = append(adds, s)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(adds)
|
||||
sort.Strings(deletes)
|
||||
|
||||
if len(adds) == 0 && len(deletes) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if len(adds) != 0 {
|
||||
message = fmt.Sprintf("added %v", strings.Join(adds, ", "))
|
||||
}
|
||||
if len(deletes) != 0 {
|
||||
if len(adds) != 0 {
|
||||
message += "; "
|
||||
}
|
||||
message += fmt.Sprintf("deleted %v", strings.Join(deletes, ", "))
|
||||
}
|
||||
return message, true
|
||||
}
|
||||
|
||||
func (rp *ResourcePlan) diffPropertyDependencies(a, b map[resource.PropertyKey][]resource.URN) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is similar to ResourcePlan.checkGoal but for the case we're we don't have a goal saved.
|
||||
// This simple checks that we're not changing anything.
|
||||
func checkMissingPlan(
|
||||
oldState *resource.State,
|
||||
newInputs resource.PropertyMap,
|
||||
programGoal *resource.Goal) error {
|
||||
|
||||
// We new up a fake ResourcePlan that matches the old state and then simply call checkGoal on it.
|
||||
goal := &GoalPlan{
|
||||
Type: oldState.Type,
|
||||
Name: oldState.URN.Name(),
|
||||
Custom: oldState.Custom,
|
||||
Adds: nil,
|
||||
Deletes: nil,
|
||||
Updates: nil,
|
||||
Parent: oldState.Parent,
|
||||
Protect: oldState.Protect,
|
||||
Dependencies: oldState.Dependencies,
|
||||
Provider: oldState.Provider,
|
||||
PropertyDependencies: oldState.PropertyDependencies,
|
||||
DeleteBeforeReplace: nil,
|
||||
IgnoreChanges: nil,
|
||||
AdditionalSecretOutputs: oldState.AdditionalSecretOutputs,
|
||||
Aliases: oldState.Aliases,
|
||||
ID: "",
|
||||
CustomTimeouts: oldState.CustomTimeouts,
|
||||
}
|
||||
|
||||
rp := ResourcePlan{Goal: goal}
|
||||
return rp.checkGoal(oldState.Outputs, newInputs, programGoal)
|
||||
}
|
||||
|
||||
func (rp *ResourcePlan) checkGoal(
|
||||
oldOutputs resource.PropertyMap,
|
||||
newInputs resource.PropertyMap,
|
||||
programGoal *resource.Goal) error {
|
||||
|
||||
contract.Assert(programGoal != nil)
|
||||
contract.Assert(newInputs != nil)
|
||||
// rp.Goal may be nil, but if it isn't Type and Name should match
|
||||
contract.Assert(rp.Goal == nil || rp.Goal.Type == programGoal.Type)
|
||||
contract.Assert(rp.Goal == nil || rp.Goal.Name == programGoal.Name)
|
||||
|
||||
if rp.Goal == nil {
|
||||
// If the plan goal is nil it expected a delete
|
||||
return fmt.Errorf("resource unexpectedly not deleted")
|
||||
}
|
||||
|
||||
// Check that either both resources are custom resources or both are component resources.
|
||||
if programGoal.Custom != rp.Goal.Custom {
|
||||
// TODO(pdg-plan): wording?
|
||||
expected := "custom"
|
||||
if !rp.Goal.Custom {
|
||||
expected = "component"
|
||||
}
|
||||
return fmt.Errorf("resource kind changed (expected %v)", expected)
|
||||
}
|
||||
|
||||
// Check that the provider is identical.
|
||||
if rp.Goal.Provider != programGoal.Provider {
|
||||
// Provider references are a combination of URN and ID, the latter of which may be unknown. Check for that
|
||||
// case here.
|
||||
expected, err := providers.ParseReference(rp.Goal.Provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse provider reference %v: %w", rp.Goal.Provider, err)
|
||||
}
|
||||
actual, err := providers.ParseReference(programGoal.Provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse provider reference %v: %w", programGoal.Provider, err)
|
||||
}
|
||||
if expected.URN() != actual.URN() || expected.ID() != providers.UnknownID {
|
||||
return fmt.Errorf("provider changed (expected %v)", rp.Goal.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the parent is identical.
|
||||
if programGoal.Parent != rp.Goal.Parent {
|
||||
return fmt.Errorf("parent changed (expected %v)", rp.Goal.Parent)
|
||||
}
|
||||
|
||||
// Check that the protect bit is identical.
|
||||
if programGoal.Protect != rp.Goal.Protect {
|
||||
return fmt.Errorf("protect changed (expected %v)", rp.Goal.Protect)
|
||||
}
|
||||
|
||||
// Check that the DBR bit is identical.
|
||||
switch {
|
||||
case rp.Goal.DeleteBeforeReplace == nil && programGoal.DeleteBeforeReplace == nil:
|
||||
// OK
|
||||
case rp.Goal.DeleteBeforeReplace != nil && programGoal.DeleteBeforeReplace != nil:
|
||||
if *rp.Goal.DeleteBeforeReplace != *programGoal.DeleteBeforeReplace {
|
||||
return fmt.Errorf("deleteBeforeReplace changed (expected %v)", *rp.Goal.DeleteBeforeReplace)
|
||||
}
|
||||
default:
|
||||
expected := "no value"
|
||||
if rp.Goal.DeleteBeforeReplace != nil {
|
||||
expected = fmt.Sprintf("%v", *rp.Goal.DeleteBeforeReplace)
|
||||
}
|
||||
return fmt.Errorf("deleteBeforeReplace changed (expected %v)", expected)
|
||||
}
|
||||
|
||||
// Check that the import ID is identical.
|
||||
if rp.Goal.ID != programGoal.ID {
|
||||
return fmt.Errorf("importID changed (expected %v)", rp.Goal.ID)
|
||||
}
|
||||
|
||||
// Check that the timeouts are identical.
|
||||
switch {
|
||||
case rp.Goal.CustomTimeouts.Create != programGoal.CustomTimeouts.Create:
|
||||
return fmt.Errorf("create timeout changed (expected %v)", rp.Goal.CustomTimeouts.Create)
|
||||
case rp.Goal.CustomTimeouts.Update != programGoal.CustomTimeouts.Update:
|
||||
return fmt.Errorf("update timeout changed (expected %v)", rp.Goal.CustomTimeouts.Update)
|
||||
case rp.Goal.CustomTimeouts.Delete != programGoal.CustomTimeouts.Delete:
|
||||
return fmt.Errorf("delete timeout changed (expected %v)", rp.Goal.CustomTimeouts.Delete)
|
||||
}
|
||||
|
||||
// Check that the ignoreChanges sets are identical.
|
||||
if message, changed := rp.diffStrings(rp.Goal.IgnoreChanges, programGoal.IgnoreChanges); changed {
|
||||
return fmt.Errorf("ignoreChanges changed: %v", message)
|
||||
}
|
||||
|
||||
// Check that the additionalSecretOutputs sets are identical.
|
||||
if message, changed := rp.diffPropertyKeys(
|
||||
rp.Goal.AdditionalSecretOutputs, programGoal.AdditionalSecretOutputs); changed {
|
||||
return fmt.Errorf("additionalSecretOutputs changed: %v", message)
|
||||
}
|
||||
|
||||
// Check that the alias sets are identical.
|
||||
if message, changed := rp.diffURNs(rp.Goal.Aliases, programGoal.Aliases); changed {
|
||||
return fmt.Errorf("aliases changed: %v", message)
|
||||
}
|
||||
|
||||
// Check that the dependencies match.
|
||||
if message, changed := rp.diffURNs(rp.Goal.Dependencies, programGoal.Dependencies); changed {
|
||||
return fmt.Errorf("dependencies changed: %v", message)
|
||||
}
|
||||
|
||||
// Check that the property diffs meet the constraints set in the plan.
|
||||
changes := []string{}
|
||||
var diff *resource.ObjectDiff
|
||||
var hasDiff bool
|
||||
if diff, hasDiff = oldOutputs.DiffIncludeUnknowns(newInputs); hasDiff {
|
||||
// Check that any adds are in the goal for adds
|
||||
for k := range diff.Adds {
|
||||
if expected, has := rp.Goal.Adds[k]; has {
|
||||
actual := diff.Adds[k]
|
||||
if !expected.DeepEqualsIncludeUnknowns(actual) {
|
||||
// diff wants to add this with value X but constraint wants to add with value Y
|
||||
changes = append(changes, "+"+string(k))
|
||||
}
|
||||
} else {
|
||||
// diff wants to add this, but not listed as an add in the constraints
|
||||
changes = append(changes, "+"+string(k))
|
||||
}
|
||||
}
|
||||
|
||||
// Check that any removes are in the goal for removes
|
||||
for k := range diff.Deletes {
|
||||
found := false
|
||||
for i := range rp.Goal.Deletes {
|
||||
if rp.Goal.Deletes[i] == k {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// diff wants to delete this, but not listed as a delete in the constraints
|
||||
changes = append(changes, "-"+string(k))
|
||||
}
|
||||
}
|
||||
|
||||
// Check that any changes are in the goal for changes or adds
|
||||
// "or adds" is because if our constraint says to add K=V and someone has already
|
||||
// added K=W we don't consider it a constraint violation to update K to V.
|
||||
// This is similar to how if we have a Create resource constraint we don't consider it
|
||||
// a violation to just update it instead of creating it.
|
||||
for k := range diff.Updates {
|
||||
actual := diff.Updates[k].New
|
||||
if expected, has := rp.Goal.Updates[k]; has {
|
||||
if !expected.DeepEqualsIncludeUnknowns(actual) {
|
||||
// diff wants to change this with value X but constraint wants to change with value Y
|
||||
changes = append(changes, "~"+string(k))
|
||||
}
|
||||
} else if expected, has := rp.Goal.Adds[k]; has {
|
||||
if !expected.DeepEqualsIncludeUnknowns(actual) {
|
||||
// diff wants to change this with value X but constraint wants to add with value Y
|
||||
changes = append(changes, "~"+string(k))
|
||||
}
|
||||
} else {
|
||||
// diff wants to update this, but not listed as an update in the constraints
|
||||
changes = append(changes, "~"+string(k))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No diff, just new up an empty ObjectDiff for checks below
|
||||
diff = &resource.ObjectDiff{}
|
||||
}
|
||||
|
||||
// Symmetric check, check that the constraints didn't expect things to happen that aren't in the new inputs
|
||||
|
||||
for k := range rp.Goal.Adds {
|
||||
// We expected an add, make sure the value is in the new inputs.
|
||||
// That means it's either an add, update, or a same, both are ok for an add constraint.
|
||||
expected := rp.Goal.Adds[k]
|
||||
|
||||
// If this is in diff.Adds or diff.Updates we'll of already checked it
|
||||
_, inAdds := diff.Adds[k]
|
||||
_, inUpdates := diff.Updates[k]
|
||||
|
||||
if !inAdds && !inUpdates {
|
||||
// It wasn't in the diff as an add or update so check we have a same
|
||||
if actual, has := newInputs[k]; has {
|
||||
if !expected.DeepEqualsIncludeUnknowns(actual) {
|
||||
// diff wants to same this with value X but constraint wants to add with value Y
|
||||
changes = append(changes, "~"+string(k))
|
||||
}
|
||||
} else {
|
||||
// Not a same, update or an add but constraint wants to add it
|
||||
changes = append(changes, "-"+string(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k := range rp.Goal.Updates {
|
||||
// We expected an update, make sure the value is in the new inputs as an update (not an add)
|
||||
expected := rp.Goal.Updates[k]
|
||||
|
||||
// If this is in diff.Updates we'll of already checked it
|
||||
_, inUpdates := diff.Updates[k]
|
||||
|
||||
if !inUpdates {
|
||||
// Check if this was in adds, it's not ok to have an update constraint but actually do an add
|
||||
_, inAdds := diff.Adds[k]
|
||||
if inAdds {
|
||||
// Constraint wants to update it, but diff wants to add it
|
||||
changes = append(changes, "+"+string(k))
|
||||
} else if actual, has := newInputs[k]; has {
|
||||
// It wasn't in the diff as an add so check we have a same
|
||||
if !expected.DeepEqualsIncludeUnknowns(actual) {
|
||||
// diff wants to same this with value X but constraint wants to update with value Y
|
||||
changes = append(changes, "~"+string(k))
|
||||
}
|
||||
} else {
|
||||
// Not a same or an update but constraint wants to update it
|
||||
changes = append(changes, "-"+string(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range rp.Goal.Deletes {
|
||||
// We expected a delete, make sure its not present
|
||||
k := rp.Goal.Deletes[i]
|
||||
|
||||
// If this is in diff.Deletes we'll of already checked it
|
||||
_, inDeletes := diff.Deletes[k]
|
||||
if !inDeletes {
|
||||
// See if this is an add, update, or same
|
||||
if _, has := diff.Adds[k]; has {
|
||||
// Constraint wants to delete this but diff wants to add it
|
||||
changes = append(changes, "+"+string(k))
|
||||
} else if _, has := diff.Updates[k]; has {
|
||||
// Constraint wants to delete this but diff wants to update it
|
||||
changes = append(changes, "~"+string(k))
|
||||
} else if _, has := diff.Sames[k]; has {
|
||||
// Constraint wants to delete this but diff wants to leave it same
|
||||
changes = append(changes, "~"+string(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) > 0 {
|
||||
// Sort changes, mostly so it's easy to write tests against determinstic strings
|
||||
sort.Strings(changes)
|
||||
return fmt.Errorf("properties changed: %v", strings.Join(changes, ", "))
|
||||
}
|
||||
|
||||
// Check that the property dependencies match. Note that because it is legal for a property that is unknown in the
|
||||
// plan to be unset in the program, we allow the omission of a property from the program's dependency set.
|
||||
for k, urns := range rp.Goal.PropertyDependencies {
|
||||
if programDeps, ok := programGoal.PropertyDependencies[k]; ok {
|
||||
if message, changed := rp.diffURNs(urns, programDeps); changed {
|
||||
return fmt.Errorf("dependencies for %v changed: %v", k, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -15,15 +15,12 @@
|
|||
package deploy
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
||||
"github.com/pulumi/pulumi/pkg/v3/secrets"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
||||
)
|
||||
|
||||
// Snapshot is a view of a collection of resources in an stack at a point in time. It describes resources; their
|
||||
|
@ -36,23 +33,6 @@ type Snapshot struct {
|
|||
PendingOperations []resource.Operation // all currently pending resource operations.
|
||||
}
|
||||
|
||||
// Manifest captures versions for all binaries used to construct this snapshot.
|
||||
type Manifest struct {
|
||||
Time time.Time // the time this snapshot was taken.
|
||||
Magic string // a magic cookie.
|
||||
Version string // the pulumi command version.
|
||||
Plugins []workspace.PluginInfo // the plugin versions also loaded.
|
||||
}
|
||||
|
||||
// NewMagic creates a magic cookie out of a manifest; this can be used to check for tampering. This ignores
|
||||
// any existing magic value already stored on the manifest.
|
||||
func (m Manifest) NewMagic() string {
|
||||
if m.Version == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Version)))
|
||||
}
|
||||
|
||||
// NewSnapshot creates a snapshot from the given arguments. The resources must be in topologically sorted order.
|
||||
// This property is not checked; for verification, please refer to the VerifyIntegrity function below.
|
||||
func NewSnapshot(manifest Manifest, secretsManager secrets.Manager,
|
||||
|
|
|
@ -1166,6 +1166,28 @@ func (op StepOp) Suffix() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// ConstrainedTo returns true if this operation is no more impactful than the constraint.
|
||||
func (op StepOp) ConstrainedTo(constraint StepOp) bool {
|
||||
var allowed []StepOp
|
||||
switch constraint {
|
||||
case OpSame, OpDelete, OpRead, OpReadReplacement, OpRefresh, OpReadDiscard, OpDiscardReplaced,
|
||||
OpRemovePendingReplace, OpImport, OpImportReplacement:
|
||||
allowed = []StepOp{constraint}
|
||||
case OpCreate:
|
||||
allowed = []StepOp{OpSame, OpCreate}
|
||||
case OpUpdate:
|
||||
allowed = []StepOp{OpSame, OpUpdate}
|
||||
case OpReplace, OpCreateReplacement, OpDeleteReplaced:
|
||||
allowed = []StepOp{OpSame, OpUpdate, constraint}
|
||||
}
|
||||
for _, candidate := range allowed {
|
||||
if candidate == op {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getProvider fetches the provider for the given step.
|
||||
func getProvider(s Step) (plugin.Provider, error) {
|
||||
if providers.IsProviderType(s.Type()) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -147,7 +148,7 @@ func (se *stepExecutor) ExecuteParallel(antichain antichain) completionToken {
|
|||
}
|
||||
|
||||
// ExecuteRegisterResourceOutputs services a RegisterResourceOutputsEvent synchronously on the calling goroutine.
|
||||
func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputsEvent) {
|
||||
func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputsEvent) result.Result {
|
||||
// Look up the final state in the pending registration list.
|
||||
urn := e.URN()
|
||||
value, has := se.pendingNews.Load(urn)
|
||||
|
@ -160,7 +161,27 @@ func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputs
|
|||
outs := e.Outputs()
|
||||
se.log(synchronousWorkerID,
|
||||
"registered resource outputs %s: old=#%d, new=#%d", urn, len(reg.New().Outputs), len(outs))
|
||||
reg.New().Outputs = e.Outputs()
|
||||
reg.New().Outputs = outs
|
||||
|
||||
// If a plan is present check that these outputs match what we recorded before
|
||||
if se.deployment.plan != nil {
|
||||
resourcePlan, ok := se.deployment.plan.ResourcePlans[urn]
|
||||
if !ok {
|
||||
return result.FromError(fmt.Errorf("no plan for resource %v", urn))
|
||||
}
|
||||
|
||||
if diffs, has := resourcePlan.Outputs.DiffIncludeUnknowns(outs); has {
|
||||
return result.FromError(fmt.Errorf("resource violates plan: %v", diffs))
|
||||
}
|
||||
}
|
||||
|
||||
// Save these new outputs to the plan
|
||||
if resourcePlan, ok := se.deployment.newPlans.get(urn); ok {
|
||||
resourcePlan.Outputs = outs
|
||||
} else {
|
||||
return result.FromError(fmt.Errorf("this should already have a plan from when we called register resources"))
|
||||
}
|
||||
|
||||
// If there is an event subscription for finishing the resource, execute them.
|
||||
if e := se.opts.Events; e != nil {
|
||||
if eventerr := e.OnResourceOutputs(reg); eventerr != nil {
|
||||
|
@ -176,10 +197,11 @@ func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputs
|
|||
diagMsg := diag.RawMessage(reg.URN(), outErr.Error())
|
||||
se.deployment.Diag().Errorf(diagMsg)
|
||||
se.cancelDueToError()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
e.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Errored returns whether or not this step executor saw a step whose execution ended in failure.
|
||||
|
@ -295,6 +317,11 @@ func (se *stepExecutor) executeStep(workerID int, step Step) error {
|
|||
if _, hasGoal := se.deployment.goals.get(newState.URN); hasGoal {
|
||||
se.deployment.news.set(newState.URN, newState)
|
||||
}
|
||||
|
||||
// Update the resource's outputs in the generated plan.
|
||||
if resourcePlan, ok := se.deployment.newPlans.get(newState.URN); ok {
|
||||
resourcePlan.Outputs = newState.Outputs
|
||||
}
|
||||
}
|
||||
|
||||
if events != nil {
|
||||
|
|
|
@ -179,6 +179,39 @@ func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, res
|
|||
contract.Assert(len(steps) == 0)
|
||||
return nil, res
|
||||
}
|
||||
|
||||
// Check each proposed step against the relevant resource plan, if any
|
||||
for _, s := range steps {
|
||||
if sg.deployment.plan != nil {
|
||||
if resourcePlan, ok := sg.deployment.plan.ResourcePlans[s.URN()]; ok {
|
||||
if len(resourcePlan.Ops) == 0 {
|
||||
return nil, result.Errorf("%v is not allowed by the plan: no more steps were expected for this resource", s.Op())
|
||||
}
|
||||
|
||||
constraint := resourcePlan.Ops[0]
|
||||
if !s.Op().ConstrainedTo(constraint) {
|
||||
return nil, result.Errorf("%v is not allowed by the plan: this resource is constrained to %v", s.Op(), constraint)
|
||||
}
|
||||
resourcePlan.Ops = resourcePlan.Ops[1:]
|
||||
} else {
|
||||
if !s.Op().ConstrainedTo(OpSame) {
|
||||
return nil, result.Errorf("%v is not allowed by the plan: no steps were expected for this resource", s.Op())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resource plan might be aliased
|
||||
urn, isAliased := sg.aliased[s.URN()]
|
||||
if !isAliased {
|
||||
urn = s.URN()
|
||||
}
|
||||
resourcePlan, ok := sg.deployment.newPlans.get(urn)
|
||||
if !ok {
|
||||
return nil, result.Errorf("Expected a new resource plan for %v", urn)
|
||||
}
|
||||
resourcePlan.Ops = append(resourcePlan.Ops, s.Op())
|
||||
}
|
||||
|
||||
if !sg.isTargetedUpdate() {
|
||||
return steps, nil
|
||||
}
|
||||
|
@ -272,6 +305,29 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
|
|||
inputs = processedInputs
|
||||
}
|
||||
|
||||
// Generate the output goal plan
|
||||
// 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.
|
||||
newResourcePlan := &ResourcePlan{Goal: NewGoalPlan(oldOutputs, goal)}
|
||||
sg.deployment.newPlans.set(urn, newResourcePlan)
|
||||
|
||||
// If there is a plan for this resource, validate that the program goal conforms to the plan.
|
||||
// If theres no plan for this resource check that nothing has been changed.
|
||||
if sg.deployment.plan != nil {
|
||||
resourcePlan, ok := sg.deployment.plan.ResourcePlans[urn]
|
||||
if !ok {
|
||||
if old == nil {
|
||||
// We could error here, but we'll trigger an error later on anyway that Create isn't valid here
|
||||
} else if err := checkMissingPlan(old, inputs, goal); err != nil {
|
||||
return nil, result.FromError(fmt.Errorf("resource violates plan: %w", err))
|
||||
}
|
||||
} else {
|
||||
if err := resourcePlan.checkGoal(oldOutputs, inputs, goal); err != nil {
|
||||
return nil, result.FromError(fmt.Errorf("resource violates plan: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produce a new state object that we'll build up as operations are performed. Ultimately, this is what will
|
||||
// get serialized into the checkpoint file.
|
||||
new := resource.NewState(goal.Type, urn, goal.Custom, false, "", inputs, nil, goal.Parent, goal.Protect, false,
|
||||
|
@ -646,6 +702,13 @@ func (sg *stepGenerator) generateStepsFromDiff(
|
|||
continue
|
||||
}
|
||||
|
||||
if _, ok := sg.deployment.newPlans.get(dependentResource.URN); !ok {
|
||||
// We haven't see this resource before, create a new
|
||||
// resource plan for it with no goal (because it's going to be a delete)
|
||||
resourcePlan := &ResourcePlan{}
|
||||
sg.deployment.newPlans.set(dependentResource.URN, resourcePlan)
|
||||
}
|
||||
|
||||
sg.dependentReplaceKeys[dependentResource.URN] = toReplace[i].keys
|
||||
|
||||
logging.V(7).Infof("Planner decided to delete '%v' due to dependence on condemned resource '%v'",
|
||||
|
@ -751,6 +814,40 @@ func (sg *stepGenerator) GenerateDeletes(targetsOpt map[resource.URN]bool) ([]St
|
|||
}
|
||||
}
|
||||
|
||||
// Check each proposed delete against the relevant resource plan
|
||||
for _, s := range dels {
|
||||
if sg.deployment.plan != nil {
|
||||
if resourcePlan, ok := sg.deployment.plan.ResourcePlans[s.URN()]; ok {
|
||||
if len(resourcePlan.Ops) == 0 {
|
||||
return nil, result.Errorf("%v is not allowed by the plan: no more steps were expected for this resource", s.Op())
|
||||
}
|
||||
|
||||
constraint := resourcePlan.Ops[0]
|
||||
// We remove the Op from the list before doing the constraint check.
|
||||
// This is because we look at Ops at the end to see if any expected operations didn't attempt to happen.
|
||||
// This op has been attempted, it just might fail its constraint.
|
||||
resourcePlan.Ops = resourcePlan.Ops[1:]
|
||||
|
||||
if !s.Op().ConstrainedTo(constraint) {
|
||||
return nil, result.Errorf("%v is not allowed by the plan: this resource is constrained to %v", s.Op(), constraint)
|
||||
}
|
||||
} else {
|
||||
if !s.Op().ConstrainedTo(OpSame) {
|
||||
return nil, result.Errorf("%v is not allowed by the plan: no steps were expected for this resource", s.Op())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resourcePlan, ok := sg.deployment.newPlans.get(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{}
|
||||
sg.deployment.newPlans.set(s.URN(), resourcePlan)
|
||||
}
|
||||
resourcePlan.Ops = append(resourcePlan.Ops, s.Op())
|
||||
}
|
||||
|
||||
// If -target was provided to either `pulumi update` or `pulumi destroy` then only delete
|
||||
// resources that were specified.
|
||||
allowedResourcesToDelete, res := sg.determineAllowedResourcesToDeleteFromTargets(targetsOpt)
|
||||
|
|
|
@ -23,8 +23,6 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
||||
"github.com/pulumi/pulumi/pkg/v3/secrets"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
||||
|
@ -32,7 +30,6 @@ import (
|
|||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
|
@ -105,23 +102,7 @@ func SerializeDeployment(snap *deploy.Snapshot, sm secrets.Manager, showSecrets
|
|||
contract.Require(snap != nil, "snap")
|
||||
|
||||
// Capture the version information into a manifest.
|
||||
manifest := apitype.ManifestV1{
|
||||
Time: snap.Manifest.Time,
|
||||
Magic: snap.Manifest.Magic,
|
||||
Version: snap.Manifest.Version,
|
||||
}
|
||||
for _, plug := range snap.Manifest.Plugins {
|
||||
var version string
|
||||
if plug.Version != nil {
|
||||
version = plug.Version.String()
|
||||
}
|
||||
manifest.Plugins = append(manifest.Plugins, apitype.PluginInfoV1{
|
||||
Name: plug.Name,
|
||||
Path: plug.Path,
|
||||
Type: plug.Kind,
|
||||
Version: version,
|
||||
})
|
||||
}
|
||||
manifest := snap.Manifest.Serialize()
|
||||
|
||||
// If a specific secrets manager was not provided, use the one in the snapshot, if present.
|
||||
if sm == nil {
|
||||
|
@ -223,25 +204,9 @@ func DeserializeUntypedDeployment(
|
|||
// DeserializeDeploymentV3 deserializes a typed DeploymentV3 into a `deploy.Snapshot`.
|
||||
func DeserializeDeploymentV3(deployment apitype.DeploymentV3, secretsProv SecretsProvider) (*deploy.Snapshot, error) {
|
||||
// Unpack the versions.
|
||||
manifest := deploy.Manifest{
|
||||
Time: deployment.Manifest.Time,
|
||||
Magic: deployment.Manifest.Magic,
|
||||
Version: deployment.Manifest.Version,
|
||||
}
|
||||
for _, plug := range deployment.Manifest.Plugins {
|
||||
var version *semver.Version
|
||||
if v := plug.Version; v != "" {
|
||||
sv, err := semver.ParseTolerant(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version = &sv
|
||||
}
|
||||
manifest.Plugins = append(manifest.Plugins, workspace.PluginInfo{
|
||||
Name: plug.Name,
|
||||
Kind: plug.Type,
|
||||
Version: version,
|
||||
})
|
||||
manifest, err := deploy.DeserializeManifest(deployment.Manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secretsManager secrets.Manager
|
||||
|
@ -295,7 +260,7 @@ func DeserializeDeploymentV3(deployment apitype.DeploymentV3, secretsProv Secret
|
|||
ops = append(ops, desop)
|
||||
}
|
||||
|
||||
return deploy.NewSnapshot(manifest, secretsManager, resources, ops), nil
|
||||
return deploy.NewSnapshot(*manifest, secretsManager, resources, ops), nil
|
||||
}
|
||||
|
||||
// SerializeResource turns a resource into a structure suitable for serialization.
|
||||
|
|
162
pkg/resource/stack/plan.go
Normal file
162
pkg/resource/stack/plan.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package stack
|
||||
|
||||
import (
|
||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
||||
)
|
||||
|
||||
func SerializeResourcePlan(
|
||||
plan *deploy.ResourcePlan,
|
||||
enc config.Encrypter,
|
||||
showSecrets bool) (apitype.ResourcePlanV1, error) {
|
||||
|
||||
adds, err := SerializeProperties(plan.Goal.Adds, enc, showSecrets)
|
||||
if err != nil {
|
||||
return apitype.ResourcePlanV1{}, err
|
||||
}
|
||||
|
||||
updates, err := SerializeProperties(plan.Goal.Adds, enc, showSecrets)
|
||||
if err != nil {
|
||||
return apitype.ResourcePlanV1{}, err
|
||||
}
|
||||
|
||||
deletes := make([]string, len(plan.Goal.Deletes))
|
||||
for i := range deletes {
|
||||
deletes[i] = string(plan.Goal.Deletes[i])
|
||||
}
|
||||
|
||||
var outputs map[string]interface{}
|
||||
if plan.Outputs != nil {
|
||||
outs, err := SerializeProperties(plan.Outputs, enc, showSecrets)
|
||||
if err != nil {
|
||||
return apitype.ResourcePlanV1{}, err
|
||||
}
|
||||
outputs = outs
|
||||
}
|
||||
|
||||
goal := apitype.GoalV1{
|
||||
Type: plan.Goal.Type,
|
||||
Name: plan.Goal.Name,
|
||||
Custom: plan.Goal.Custom,
|
||||
Adds: adds,
|
||||
Deletes: deletes,
|
||||
Updates: updates,
|
||||
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,
|
||||
Outputs: outputs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func SerializePlan(plan *deploy.Plan, enc config.Encrypter, showSecrets bool) (apitype.DeploymentPlanV1, error) {
|
||||
resourcePlans := map[resource.URN]apitype.ResourcePlanV1{}
|
||||
for urn, plan := range plan.ResourcePlans {
|
||||
serializedPlan, err := SerializeResourcePlan(plan, enc, showSecrets)
|
||||
if err != nil {
|
||||
return apitype.DeploymentPlanV1{}, err
|
||||
}
|
||||
resourcePlans[urn] = serializedPlan
|
||||
}
|
||||
|
||||
return apitype.DeploymentPlanV1{
|
||||
Manifest: plan.Manifest.Serialize(),
|
||||
ResourcePlans: resourcePlans,
|
||||
Config: plan.Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DeserializeResourcePlan(
|
||||
plan apitype.ResourcePlanV1,
|
||||
dec config.Decrypter,
|
||||
enc config.Encrypter) (*deploy.ResourcePlan, error) {
|
||||
|
||||
adds, err := DeserializeProperties(plan.Goal.Adds, dec, enc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updates, err := DeserializeProperties(plan.Goal.Updates, dec, enc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var outputs resource.PropertyMap
|
||||
if plan.Outputs != nil {
|
||||
outs, err := DeserializeProperties(plan.Outputs, dec, enc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputs = outs
|
||||
}
|
||||
|
||||
goal := &deploy.GoalPlan{
|
||||
Type: plan.Goal.Type,
|
||||
Name: plan.Goal.Name,
|
||||
Custom: plan.Goal.Custom,
|
||||
Adds: adds,
|
||||
Deletes: nil,
|
||||
Updates: updates,
|
||||
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,
|
||||
Outputs: outputs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DeserializePlan(plan apitype.DeploymentPlanV1, dec config.Decrypter, enc config.Encrypter) (*deploy.Plan, error) {
|
||||
manifest, err := deploy.DeserializeManifest(plan.Manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deserializedPlan := &deploy.Plan{
|
||||
Config: plan.Config,
|
||||
Manifest: *manifest,
|
||||
}
|
||||
for urn, resourcePlan := range plan.ResourcePlans {
|
||||
deserializedResourcePlan, err := DeserializeResourcePlan(resourcePlan, dec, enc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deserializedPlan.ResourcePlans[urn] = deserializedResourcePlan
|
||||
}
|
||||
return deserializedPlan, nil
|
||||
}
|
|
@ -62,6 +62,7 @@ require (
|
|||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.3 // indirect
|
||||
github.com/src-d/gcfg v1.4.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
||||
|
|
79
sdk/go/common/apitype/plan.go
Normal file
79
sdk/go/common/apitype/plan.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package apitype
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
||||
"github.com/pulumi/pulumi/sdk/v3/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 properties that will be added.
|
||||
Adds map[string]interface{} `json:"adds,omitempty"`
|
||||
// the resource properties that will be deleted.
|
||||
Deletes []string `json:"deletes,omitempty"`
|
||||
// the resource properties that will be updated.
|
||||
Updates map[string]interface{} `json:"updates,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"`
|
||||
// The proposed outputs for the resource, if any. Purely advisory.
|
||||
Outputs map[string]interface{} `json:"state"`
|
||||
}
|
||||
|
||||
// VersionedDeploymentPlan is a version number plus a JSON document. The version number describes what
|
||||
// version of the DeploymentPlan structure the DeploymentPlan member's JSON document can decode into.
|
||||
type VersionedDeploymentPlan struct {
|
||||
Version int `json:"version"`
|
||||
Plan json.RawMessage `json:"plan"`
|
||||
}
|
||||
|
||||
// DeploymentPlanV1 is the serializable version of a deployment plan.
|
||||
type DeploymentPlanV1 struct {
|
||||
// TODO(pdg-plan): should there be a message here?
|
||||
|
||||
// Manifest contains metadata about this plan.
|
||||
Manifest ManifestV1 `json:"manifest" yaml:"manifest"`
|
||||
// Any environment variables that were set when the plan was created. Values are encrypted.
|
||||
EnvironmentVariables map[string][]byte `json:"environmentVariables,omitempty"`
|
||||
// The configuration in use during the plan.
|
||||
Config config.Map `json:"config,omitempty"`
|
||||
|
||||
// The set of resource plans.
|
||||
ResourcePlans map[resource.URN]ResourcePlanV1 `json:"resourcePlans,omitempty"`
|
||||
}
|
|
@ -646,3 +646,71 @@ const OutputValueSig = "d0e6a833031e9bbcd3f4e8bde6ca49a4"
|
|||
func IsInternalPropertyKey(key PropertyKey) bool {
|
||||
return strings.HasPrefix(string(key), "__")
|
||||
}
|
||||
|
||||
// DeepCopy creates a new copy of this property map.
|
||||
func (m PropertyMap) DeepCopy() PropertyMap {
|
||||
copy := PropertyMap{}
|
||||
for k, v := range m {
|
||||
copy[k] = v.DeepCopy()
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// DeepCopy creates a new copy of this property.
|
||||
func (v PropertyValue) DeepCopy() PropertyValue {
|
||||
switch {
|
||||
case v.IsArray():
|
||||
copy := make([]PropertyValue, len(v.ArrayValue()))
|
||||
for i, v := range v.ArrayValue() {
|
||||
copy[i] = v.DeepCopy()
|
||||
}
|
||||
return NewArrayProperty(copy)
|
||||
case v.IsObject():
|
||||
return NewObjectProperty(v.ObjectValue().DeepCopy())
|
||||
case v.IsSecret():
|
||||
return MakeSecret(v.SecretValue().Element.DeepCopy())
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// FindUnknowns returns the set of paths to unknown values nested inside this property map.
|
||||
func (m PropertyMap) FindUnknowns() []PropertyPath {
|
||||
var paths []PropertyPath
|
||||
for k, v := range m {
|
||||
if v.IsComputed() || v.IsOutput() {
|
||||
paths = append(paths, PropertyPath{string(k)})
|
||||
} else {
|
||||
for _, p := range v.FindUnknowns() {
|
||||
p = append(PropertyPath{string(k)}, p...)
|
||||
paths = append(paths, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// FindUnknowns returns the set of paths to unknown values nested inside this property map.
|
||||
func (v PropertyValue) FindUnknowns() []PropertyPath {
|
||||
switch {
|
||||
case v.IsArray():
|
||||
var paths []PropertyPath
|
||||
for i, v := range v.ArrayValue() {
|
||||
if v.IsComputed() || v.IsOutput() {
|
||||
paths = append(paths, PropertyPath{i})
|
||||
} else {
|
||||
for _, p := range v.FindUnknowns() {
|
||||
p = append(PropertyPath{i}, p...)
|
||||
paths = append(paths, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
case v.IsObject():
|
||||
return v.ObjectValue().FindUnknowns()
|
||||
case v.IsSecret():
|
||||
return v.SecretValue().Element.FindUnknowns()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ func (diff *ObjectDiff) Keys() []PropertyKey {
|
|||
type ValueDiff struct {
|
||||
Old PropertyValue // the old value.
|
||||
New PropertyValue // the new value.
|
||||
Secret *ValueDiff // the secret value diff (only for secrets)
|
||||
Array *ArrayDiff // the array's detailed diffs (only for arrays).
|
||||
Object *ObjectDiff // the object's detailed diffs (only for objects).
|
||||
}
|
||||
|
@ -119,8 +120,7 @@ func (diff *ArrayDiff) Len() int {
|
|||
// IgnoreKeyFunc is the callback type for Diff's ignore option.
|
||||
type IgnoreKeyFunc func(key PropertyKey) bool
|
||||
|
||||
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
|
||||
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
|
||||
func (props PropertyMap) diff(other PropertyMap, ignoreUnknowns bool, ignoreKeys []IgnoreKeyFunc) *ObjectDiff {
|
||||
adds := make(PropertyMap)
|
||||
deletes := make(PropertyMap)
|
||||
sames := make(PropertyMap)
|
||||
|
@ -137,7 +137,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
|
|||
|
||||
// First find any updates or deletes.
|
||||
for k, old := range props {
|
||||
if ignore(k) {
|
||||
if ignore(k) || ignoreUnknowns && (old.IsComputed() || old.IsOutput()) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
|
|||
// If a new exists, use it; for output properties, however, ignore differences.
|
||||
if new.IsOutput() {
|
||||
sames[k] = old
|
||||
} else if diff := old.Diff(new, ignoreKeys...); diff != nil {
|
||||
} else if diff := old.diff(new, ignoreUnknowns, ignoreKeys); diff != nil {
|
||||
if !old.HasValue() {
|
||||
adds[k] = new
|
||||
} else if !new.HasValue() {
|
||||
|
@ -156,7 +156,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
|
|||
} else {
|
||||
sames[k] = old
|
||||
}
|
||||
} else if old.HasValue() {
|
||||
} else if old.HasValue() && (!ignoreUnknowns || !old.IsComputed()) {
|
||||
// If there was no new property, it has been deleted.
|
||||
deletes[k] = old
|
||||
}
|
||||
|
@ -185,8 +185,12 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
|
|||
}
|
||||
}
|
||||
|
||||
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
|
||||
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
|
||||
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
|
||||
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
|
||||
return props.diff(other, false, ignoreKeys)
|
||||
}
|
||||
|
||||
func (v PropertyValue) diff(other PropertyValue, ignoreUnknowns bool, ignoreKeys []IgnoreKeyFunc) *ValueDiff {
|
||||
if v.IsArray() && other.IsArray() {
|
||||
old := v.ArrayValue()
|
||||
new := other.ArrayValue()
|
||||
|
@ -204,7 +208,7 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
|
|||
sames := make(map[int]PropertyValue)
|
||||
updates := make(map[int]ValueDiff)
|
||||
for i := 0; i < len(old) && i < len(new); i++ {
|
||||
if diff := old[i].Diff(new[i]); diff != nil {
|
||||
if diff := old[i].diff(new[i], ignoreUnknowns, ignoreKeys); diff != nil {
|
||||
updates[i] = *diff
|
||||
} else {
|
||||
sames[i] = old[i]
|
||||
|
@ -228,7 +232,7 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
|
|||
if v.IsObject() && other.IsObject() {
|
||||
old := v.ObjectValue()
|
||||
new := other.ObjectValue()
|
||||
if diff := old.Diff(new, ignoreKeys...); diff != nil {
|
||||
if diff := old.diff(new, ignoreUnknowns, ignoreKeys); diff != nil {
|
||||
return &ValueDiff{
|
||||
Old: v,
|
||||
New: other,
|
||||
|
@ -237,21 +241,51 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
|
|||
}
|
||||
return nil
|
||||
}
|
||||
if v.IsSecret() && other.IsSecret() {
|
||||
old, new := v.SecretValue().Element, other.SecretValue().Element
|
||||
diff := old.diff(new, ignoreUnknowns, ignoreKeys)
|
||||
if diff != nil {
|
||||
return &ValueDiff{
|
||||
Old: v,
|
||||
New: other,
|
||||
Secret: diff,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if v.IsResourceReference() && other.IsResourceReference() {
|
||||
vr := v.ResourceReferenceValue()
|
||||
or := other.ResourceReferenceValue()
|
||||
|
||||
if vr.URN != or.URN {
|
||||
return &ValueDiff{Old: v, New: other}
|
||||
}
|
||||
|
||||
vid, oid := vr.ID, or.ID
|
||||
if vid.IsComputed() && ignoreUnknowns || vid.DeepEquals(oid) {
|
||||
return nil
|
||||
}
|
||||
return &ValueDiff{Old: v, New: other}
|
||||
}
|
||||
|
||||
// If we got here, either the values are primitives, or they weren't the same type; do a simple diff.
|
||||
if v.DeepEquals(other) {
|
||||
if v.DeepEquals(other) || ignoreUnknowns && (v.IsComputed() || v.IsOutput()) {
|
||||
return nil
|
||||
}
|
||||
return &ValueDiff{Old: v, New: other}
|
||||
}
|
||||
|
||||
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
|
||||
func (props PropertyMap) DeepEquals(other PropertyMap) bool {
|
||||
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
|
||||
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
|
||||
return v.diff(other, false, ignoreKeys)
|
||||
}
|
||||
|
||||
func (props PropertyMap) deepEquals(other PropertyMap, includeUnknowns bool) bool {
|
||||
// If any in props either doesn't exist, or is of a different value, return false.
|
||||
for _, k := range props.StableKeys() {
|
||||
v := props[k]
|
||||
if p, has := other[k]; has {
|
||||
if !v.DeepEquals(p) {
|
||||
if !v.deepEquals(p, includeUnknowns) {
|
||||
return false
|
||||
}
|
||||
} else if v.HasValue() {
|
||||
|
@ -269,8 +303,19 @@ func (props PropertyMap) DeepEquals(other PropertyMap) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
|
||||
func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
||||
func (v PropertyValue) deepEquals(other PropertyValue, includeUnknowns bool) bool {
|
||||
// Computed values are always equal.
|
||||
if v.IsComputed() && other.IsComputed() {
|
||||
return true
|
||||
}
|
||||
|
||||
// If includeUnknowns is true then anything is equal to a computed
|
||||
if includeUnknowns {
|
||||
if v.IsComputed() || other.IsComputed() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Arrays are equal if they are both of the same size and elements are deeply equal.
|
||||
if v.IsArray() {
|
||||
if !other.IsArray() {
|
||||
|
@ -282,7 +327,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
|||
return false
|
||||
}
|
||||
for i, elem := range va {
|
||||
if !elem.DeepEquals(oa[i]) {
|
||||
if !elem.deepEquals(oa[i], includeUnknowns) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -309,7 +354,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
|||
}
|
||||
vo := v.ObjectValue()
|
||||
oa := other.ObjectValue()
|
||||
return vo.DeepEquals(oa)
|
||||
return vo.deepEquals(oa, includeUnknowns)
|
||||
}
|
||||
|
||||
// Secret are equal if the value they wrap are equal.
|
||||
|
@ -320,7 +365,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
|||
vs := v.SecretValue()
|
||||
os := other.SecretValue()
|
||||
|
||||
return vs.Element.DeepEquals(os.Element)
|
||||
return vs.Element.deepEquals(os.Element, includeUnknowns)
|
||||
}
|
||||
|
||||
// Resource references are equal if they refer to the same resource. The package version is ignored.
|
||||
|
@ -339,7 +384,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
|||
if vid.IsComputed() && oid.IsComputed() {
|
||||
return true
|
||||
}
|
||||
return vid.DeepEquals(oid)
|
||||
return vid.deepEquals(oid, includeUnknowns)
|
||||
}
|
||||
|
||||
// Outputs are equal if each of their fields is deeply equal.
|
||||
|
@ -367,9 +412,28 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
|||
}
|
||||
}
|
||||
|
||||
return vo.Element.DeepEquals(oo.Element)
|
||||
return vo.Element.deepEquals(oo.Element, includeUnknowns)
|
||||
}
|
||||
|
||||
// For all other cases, primitives are equal if their values are equal.
|
||||
return v.V == other.V
|
||||
}
|
||||
|
||||
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
|
||||
func (props PropertyMap) DeepEquals(other PropertyMap) bool {
|
||||
return props.deepEquals(other, false)
|
||||
}
|
||||
|
||||
// DeepEquals returns true if this property value is deeply equal to the other property value; and false otherwise.
|
||||
func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
||||
return v.deepEquals(other, false)
|
||||
}
|
||||
|
||||
func (props PropertyMap) DiffIncludeUnknowns(other PropertyMap) (*ObjectDiff, bool) {
|
||||
diff := props.diff(other, true, nil)
|
||||
return diff, diff != nil
|
||||
}
|
||||
|
||||
func (v PropertyValue) DeepEqualsIncludeUnknowns(other PropertyValue) bool {
|
||||
return v.deepEquals(other, true)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -291,3 +293,39 @@ func (p PropertyPath) Contains(other PropertyPath) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
func requiresQuote(c rune) bool {
|
||||
return !(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_')
|
||||
}
|
||||
|
||||
func (p PropertyPath) String() string {
|
||||
var buf bytes.Buffer
|
||||
for i, k := range p {
|
||||
switch k := k.(type) {
|
||||
case string:
|
||||
var keyBuf bytes.Buffer
|
||||
quoted := false
|
||||
for _, c := range k {
|
||||
if requiresQuote(c) {
|
||||
quoted = true
|
||||
if c == '"' {
|
||||
keyBuf.WriteByte('\\')
|
||||
}
|
||||
}
|
||||
keyBuf.WriteRune(c)
|
||||
}
|
||||
if !quoted {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(&buf, "%s", keyBuf.String())
|
||||
} else {
|
||||
fmt.Fprintf(&buf, ".%s", keyBuf.String())
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(&buf, `["%s"]`, keyBuf.String())
|
||||
}
|
||||
case int:
|
||||
fmt.Fprintf(&buf, "[%d]", k)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
|
|
@ -49,68 +49,84 @@ func TestPropertyPath(t *testing.T) {
|
|||
}))
|
||||
|
||||
cases := []struct {
|
||||
path string
|
||||
parsed PropertyPath
|
||||
path string
|
||||
parsed PropertyPath
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"root",
|
||||
PropertyPath{"root"},
|
||||
"root",
|
||||
},
|
||||
{
|
||||
"root.nested",
|
||||
PropertyPath{"root", "nested"},
|
||||
"root.nested",
|
||||
},
|
||||
{
|
||||
`root["nested"]`,
|
||||
PropertyPath{"root", "nested"},
|
||||
`root.nested`,
|
||||
},
|
||||
{
|
||||
"root.double.nest",
|
||||
PropertyPath{"root", "double", "nest"},
|
||||
"root.double.nest",
|
||||
},
|
||||
{
|
||||
`root["double"].nest`,
|
||||
PropertyPath{"root", "double", "nest"},
|
||||
`root.double.nest`,
|
||||
},
|
||||
{
|
||||
`root["double"]["nest"]`,
|
||||
PropertyPath{"root", "double", "nest"},
|
||||
`root.double.nest`,
|
||||
},
|
||||
{
|
||||
"root.array[0]",
|
||||
PropertyPath{"root", "array", 0},
|
||||
"root.array[0]",
|
||||
},
|
||||
{
|
||||
"root.array[1]",
|
||||
PropertyPath{"root", "array", 1},
|
||||
"root.array[1]",
|
||||
},
|
||||
{
|
||||
"root.array[0].nested",
|
||||
PropertyPath{"root", "array", 0, "nested"},
|
||||
"root.array[0].nested",
|
||||
},
|
||||
{
|
||||
"root.array2[0][1].nested",
|
||||
PropertyPath{"root", "array2", 0, 1, "nested"},
|
||||
"root.array2[0][1].nested",
|
||||
},
|
||||
{
|
||||
"root.nested.array[0].double[1]",
|
||||
PropertyPath{"root", "nested", "array", 0, "double", 1},
|
||||
"root.nested.array[0].double[1]",
|
||||
},
|
||||
{
|
||||
`root["key with \"escaped\" quotes"]`,
|
||||
PropertyPath{"root", `key with "escaped" quotes`},
|
||||
`root["key with \"escaped\" quotes"]`,
|
||||
},
|
||||
{
|
||||
`root["key with a ."]`,
|
||||
PropertyPath{"root", "key with a ."},
|
||||
`root["key with a ."]`,
|
||||
},
|
||||
{
|
||||
`["root key with \"escaped\" quotes"].nested`,
|
||||
PropertyPath{`root key with "escaped" quotes`, "nested"},
|
||||
`["root key with \"escaped\" quotes"].nested`,
|
||||
},
|
||||
{
|
||||
`["root key with a ."][1]`,
|
||||
PropertyPath{"root key with a .", 1},
|
||||
`["root key with a ."][1]`,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -119,6 +135,7 @@ func TestPropertyPath(t *testing.T) {
|
|||
parsed, err := ParsePropertyPath(c.path)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.parsed, parsed)
|
||||
assert.Equal(t, c.expected, parsed.String())
|
||||
|
||||
v, ok := parsed.Get(value)
|
||||
assert.True(t, ok)
|
||||
|
|
Loading…
Reference in a new issue