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/backend/display"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/engine"
|
"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/apitype"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
"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.
|
// 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,
|
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 {
|
func ActionLabel(kind apitype.UpdateKind, dryRun bool) string {
|
||||||
v := updateTextMap[kind]
|
v := updateTextMap[kind]
|
||||||
|
@ -79,7 +80,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func PreviewThenPrompt(ctx context.Context, kind apitype.UpdateKind, stack Stack,
|
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
|
// 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
|
// 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,
|
ShowLink: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
changes, res := apply(ctx, kind, stack, op, opts, eventsChannel)
|
plan, changes, res := apply(ctx, kind, stack, op, opts, eventsChannel)
|
||||||
if res != nil {
|
if res != nil {
|
||||||
close(eventsChannel)
|
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 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 {
|
if op.Opts.AutoApprove || kind == apitype.PreviewUpdate {
|
||||||
close(eventsChannel)
|
close(eventsChannel)
|
||||||
return changes, nil
|
return plan, changes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, ensure the user wants to proceed.
|
// Otherwise, ensure the user wants to proceed.
|
||||||
res = confirmBeforeUpdating(kind, stack, events, op.Opts)
|
res = confirmBeforeUpdating(kind, stack, events, op.Opts)
|
||||||
close(eventsChannel)
|
close(eventsChannel)
|
||||||
return changes, res
|
return plan, changes, res
|
||||||
}
|
}
|
||||||
|
|
||||||
// confirmBeforeUpdating asks the user whether to proceed. A nil error means yes.
|
// 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.
|
// Preview the operation to the user and ask them if they want to proceed.
|
||||||
|
|
||||||
if !op.Opts.SkipPreview {
|
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 {
|
if res != nil || kind == apitype.PreviewUpdate {
|
||||||
return changes, res
|
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.
|
// 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,
|
DryRun: false,
|
||||||
ShowLink: true,
|
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 {
|
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)
|
RenameStack(ctx context.Context, stack Stack, newName tokens.QName) (StackReference, error)
|
||||||
|
|
||||||
// Preview shows what would be updated given the current workspace's contents.
|
// 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 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)
|
Update(ctx context.Context, stack Stack, op UpdateOperation) (engine.ResourceChanges, result.Result)
|
||||||
// Import imports resources into a stack.
|
// Import imports resources into a stack.
|
||||||
|
|
|
@ -461,12 +461,12 @@ func (b *localBackend) PackPolicies(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *localBackend) Preview(ctx context.Context, stack backend.Stack,
|
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)) {
|
if cmdutil.IsTruthy(os.Getenv(PulumiFilestateLockingEnvVar)) {
|
||||||
err := b.Lock(ctx, stack.Ref())
|
err := b.Lock(ctx, stack.Ref())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer b.Unlock(ctx, stack.Ref())
|
defer b.Unlock(ctx, stack.Ref())
|
||||||
}
|
}
|
||||||
|
@ -548,7 +548,7 @@ func (b *localBackend) Watch(ctx context.Context, stack backend.Stack,
|
||||||
func (b *localBackend) apply(
|
func (b *localBackend) apply(
|
||||||
ctx context.Context, kind apitype.UpdateKind, stack backend.Stack,
|
ctx context.Context, kind apitype.UpdateKind, stack backend.Stack,
|
||||||
op backend.UpdateOperation, opts backend.ApplierOptions,
|
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()
|
stackRef := stack.Ref()
|
||||||
stackName := stackRef.Name()
|
stackName := stackRef.Name()
|
||||||
|
@ -563,7 +563,7 @@ func (b *localBackend) apply(
|
||||||
// Start the update.
|
// Start the update.
|
||||||
update, err := b.newUpdate(stackName, op)
|
update, err := b.newUpdate(stackName, op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn a display loop to show events on the CLI.
|
// Spawn a display loop to show events on the CLI.
|
||||||
|
@ -604,19 +604,20 @@ func (b *localBackend) apply(
|
||||||
|
|
||||||
// Perform the update
|
// Perform the update
|
||||||
start := time.Now().Unix()
|
start := time.Now().Unix()
|
||||||
|
var plan *deploy.Plan
|
||||||
var changes engine.ResourceChanges
|
var changes engine.ResourceChanges
|
||||||
var updateRes result.Result
|
var updateRes result.Result
|
||||||
switch kind {
|
switch kind {
|
||||||
case apitype.PreviewUpdate:
|
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:
|
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:
|
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:
|
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:
|
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:
|
default:
|
||||||
contract.Failf("Unrecognized update kind: %s", kind)
|
contract.Failf("Unrecognized update kind: %s", kind)
|
||||||
}
|
}
|
||||||
|
@ -660,16 +661,16 @@ func (b *localBackend) apply(
|
||||||
|
|
||||||
if updateRes != nil {
|
if updateRes != nil {
|
||||||
// We swallow saveErr and backupErr as they are less important than the updateErr.
|
// We swallow saveErr and backupErr as they are less important than the updateErr.
|
||||||
return changes, updateRes
|
return plan, changes, updateRes
|
||||||
}
|
}
|
||||||
|
|
||||||
if saveErr != nil {
|
if saveErr != nil {
|
||||||
// We swallow backupErr as it is less important than the saveErr.
|
// 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 {
|
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.
|
// 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.
|
// 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)
|
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)
|
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,
|
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.
|
// We can skip PreviewtThenPromptThenExecute, and just go straight to Execute.
|
||||||
opts := backend.ApplierOptions{
|
opts := backend.ApplierOptions{
|
||||||
DryRun: true,
|
DryRun: true,
|
||||||
|
@ -931,7 +931,7 @@ func (b *cloudBackend) createAndStartUpdate(
|
||||||
func (b *cloudBackend) apply(
|
func (b *cloudBackend) apply(
|
||||||
ctx context.Context, kind apitype.UpdateKind, stack backend.Stack,
|
ctx context.Context, kind apitype.UpdateKind, stack backend.Stack,
|
||||||
op backend.UpdateOperation, opts backend.ApplierOptions,
|
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)
|
actionLabel := backend.ActionLabel(kind, opts.DryRun)
|
||||||
|
|
||||||
|
@ -945,7 +945,7 @@ func (b *cloudBackend) apply(
|
||||||
update, version, token, err :=
|
update, version, token, err :=
|
||||||
b.createAndStartUpdate(ctx, kind, stack, &op, opts.DryRun)
|
b.createAndStartUpdate(ctx, kind, stack, &op, opts.DryRun)
|
||||||
if err != nil {
|
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 {
|
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(
|
func (b *cloudBackend) runEngineAction(
|
||||||
ctx context.Context, kind apitype.UpdateKind, stackRef backend.StackReference,
|
ctx context.Context, kind apitype.UpdateKind, stackRef backend.StackReference,
|
||||||
op backend.UpdateOperation, update client.UpdateIdentifier, token string,
|
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")
|
contract.Assertf(token != "", "persisted actions require a token")
|
||||||
u, err := b.newUpdate(ctx, stackRef, op, update, token)
|
u, err := b.newUpdate(ctx, stackRef, op, update, token)
|
||||||
if err != nil {
|
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
|
// 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()
|
engineCtx.ParentSpan = parentSpan.Context()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var plan *deploy.Plan
|
||||||
var changes engine.ResourceChanges
|
var changes engine.ResourceChanges
|
||||||
var res result.Result
|
var res result.Result
|
||||||
switch kind {
|
switch kind {
|
||||||
case apitype.PreviewUpdate:
|
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:
|
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:
|
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:
|
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:
|
case apitype.DestroyUpdate:
|
||||||
changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun)
|
_, changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun)
|
||||||
default:
|
default:
|
||||||
contract.Failf("Unrecognized update kind: %s", kind)
|
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)))
|
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 {
|
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)
|
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)
|
return backend.PreviewStack(ctx, s, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ type MockBackend struct {
|
||||||
LogoutAllF func() error
|
LogoutAllF func() error
|
||||||
CurrentUserF func() (string, error)
|
CurrentUserF func() (string, error)
|
||||||
PreviewF func(context.Context, Stack,
|
PreviewF func(context.Context, Stack,
|
||||||
UpdateOperation) (engine.ResourceChanges, result.Result)
|
UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result)
|
||||||
UpdateF func(context.Context, Stack,
|
UpdateF func(context.Context, Stack,
|
||||||
UpdateOperation) (engine.ResourceChanges, result.Result)
|
UpdateOperation) (engine.ResourceChanges, result.Result)
|
||||||
ImportF func(context.Context, Stack,
|
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,
|
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 {
|
if be.PreviewF != nil {
|
||||||
return be.PreviewF(ctx, stack, op)
|
return be.PreviewF(ctx, stack, op)
|
||||||
|
@ -335,7 +335,7 @@ type MockStack struct {
|
||||||
ConfigF func() config.Map
|
ConfigF func() config.Map
|
||||||
SnapshotF func(ctx context.Context) (*deploy.Snapshot, error)
|
SnapshotF func(ctx context.Context) (*deploy.Snapshot, error)
|
||||||
BackendF func() Backend
|
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)
|
UpdateF func(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
|
||||||
ImportF func(ctx context.Context, op UpdateOperation,
|
ImportF func(ctx context.Context, op UpdateOperation,
|
||||||
imports []deploy.Import) (engine.ResourceChanges, result.Result)
|
imports []deploy.Import) (engine.ResourceChanges, result.Result)
|
||||||
|
@ -381,7 +381,10 @@ func (ms *MockStack) Backend() Backend {
|
||||||
panic("not implemented")
|
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 {
|
if ms.PreviewF != nil {
|
||||||
return ms.PreviewF(ctx, op)
|
return ms.PreviewF(ctx, op)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ type Stack interface {
|
||||||
Backend() Backend // the backend this stack belongs to.
|
Backend() Backend // the backend this stack belongs to.
|
||||||
|
|
||||||
// Preview changes to this stack.
|
// 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 this stack.
|
||||||
Update(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
|
Update(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
|
||||||
// Import resources into this stack.
|
// 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.
|
// 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)
|
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.
|
// 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)
|
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"))
|
op.Opts.Display.Color.Colorize(colors.SpecImportant+"Updating..."+colors.Reset+"\n"))
|
||||||
|
|
||||||
// Perform the update operation
|
// 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 {
|
if res != nil {
|
||||||
logging.V(5).Infof("watch update failed: %v", res.Error())
|
logging.V(5).Infof("watch update failed: %v", res.Error())
|
||||||
if res.Error() == context.Canceled {
|
if res.Error() == context.Canceled {
|
||||||
|
|
|
@ -38,6 +38,8 @@ func newPreviewCmd() *cobra.Command {
|
||||||
var configArray []string
|
var configArray []string
|
||||||
var configPath bool
|
var configPath bool
|
||||||
var client string
|
var client string
|
||||||
|
var planFilePath string
|
||||||
|
var showSecrets bool
|
||||||
|
|
||||||
// Flags for engine.UpdateOptions.
|
// Flags for engine.UpdateOptions.
|
||||||
var jsonDisplay bool
|
var jsonDisplay bool
|
||||||
|
@ -184,7 +186,7 @@ func newPreviewCmd() *cobra.Command {
|
||||||
Display: displayOpts,
|
Display: displayOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
changes, res := s.Preview(commandContext(), backend.UpdateOperation{
|
plan, changes, res := s.Preview(commandContext(), backend.UpdateOperation{
|
||||||
Proj: proj,
|
Proj: proj,
|
||||||
Root: root,
|
Root: root,
|
||||||
M: m,
|
M: m,
|
||||||
|
@ -200,6 +202,16 @@ func newPreviewCmd() *cobra.Command {
|
||||||
case expectNop && changes != nil && changes.HasChanges():
|
case expectNop && changes != nil && changes.HasChanges():
|
||||||
return result.FromError(errors.New("error: no changes were expected but changes were proposed"))
|
return result.FromError(errors.New("error: no changes were expected but changes were proposed"))
|
||||||
default:
|
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
|
return nil
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -223,6 +235,13 @@ func newPreviewCmd() *cobra.Command {
|
||||||
cmd.PersistentFlags().BoolVar(
|
cmd.PersistentFlags().BoolVar(
|
||||||
&configPath, "config-path", false,
|
&configPath, "config-path", false,
|
||||||
"Config keys contain a path to a property in a map or list to set")
|
"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(
|
cmd.PersistentFlags().StringVar(
|
||||||
&client, "client", "", "The address of an existing language runtime host to connect to")
|
&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 replaces []string
|
||||||
var targetReplaces []string
|
var targetReplaces []string
|
||||||
var targetDependents bool
|
var targetDependents bool
|
||||||
|
var planFilePath string
|
||||||
|
|
||||||
// up implementation used when the source of the Pulumi program is in the current working directory.
|
// up implementation used when the source of the Pulumi program is in the current working directory.
|
||||||
upWorkingDirectory := func(opts backend.UpdateOptions) result.Result {
|
upWorkingDirectory := func(opts backend.UpdateOptions) result.Result {
|
||||||
|
@ -143,6 +144,22 @@ func newUpCmd() *cobra.Command {
|
||||||
TargetDependents: targetDependents,
|
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{
|
changes, res := s.Update(commandContext(), backend.UpdateOperation{
|
||||||
Proj: proj,
|
Proj: proj,
|
||||||
Root: root,
|
Root: root,
|
||||||
|
@ -505,6 +522,14 @@ func newUpCmd() *cobra.Command {
|
||||||
&yes, "yes", "y", false,
|
&yes, "yes", "y", false,
|
||||||
"Automatically approve and perform the update after previewing it")
|
"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() {
|
if hasDebugCommands() {
|
||||||
cmd.PersistentFlags().StringVar(
|
cmd.PersistentFlags().StringVar(
|
||||||
&eventLogPath, "event-log", "",
|
&eventLogPath, "event-log", "",
|
||||||
|
|
|
@ -42,12 +42,15 @@ import (
|
||||||
"github.com/pulumi/pulumi/pkg/v3/backend/httpstate"
|
"github.com/pulumi/pulumi/pkg/v3/backend/httpstate"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/backend/state"
|
"github.com/pulumi/pulumi/pkg/v3/backend/state"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/engine"
|
"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/resource/stack"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/secrets/passphrase"
|
"github.com/pulumi/pulumi/pkg/v3/secrets/passphrase"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/util/cancel"
|
"github.com/pulumi/pulumi/pkg/v3/util/cancel"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/util/tracing"
|
"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/constant"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
"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/ciutil"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
"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
|
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) {
|
func buildStackName(stackName string) (string, error) {
|
||||||
if strings.Count(stackName, "/") == 2 {
|
if strings.Count(stackName, "/") == 2 {
|
||||||
return stackName, nil
|
return stackName, nil
|
||||||
|
|
|
@ -167,7 +167,7 @@ func newDeployment(ctx *Context, info *deploymentContext, opts deploymentOptions
|
||||||
var depl *deploy.Deployment
|
var depl *deploy.Deployment
|
||||||
if !opts.isImport {
|
if !opts.isImport {
|
||||||
depl, err = deploy.NewDeployment(
|
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 {
|
} else {
|
||||||
_, defaultProviderVersions, pluginErr := installPlugins(proj, pwd, main, target, plugctx,
|
_, defaultProviderVersions, pluginErr := installPlugins(proj, pwd, main, target, plugctx,
|
||||||
false /*returnInstallErrors*/)
|
false /*returnInstallErrors*/)
|
||||||
|
@ -218,12 +218,12 @@ type runActions interface {
|
||||||
|
|
||||||
// run executes the deployment. It is primarily responsible for handling cancellation.
|
// run executes the deployment. It is primarily responsible for handling cancellation.
|
||||||
func (deployment *deployment) run(cancelCtx *Context, actions runActions, policyPacks map[string]string,
|
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.
|
// Change into the plugin context's working directory.
|
||||||
chdir, err := fsutil.Chdir(deployment.Plugctx.Pwd)
|
chdir, err := fsutil.Chdir(deployment.Plugctx.Pwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer chdir()
|
defer chdir()
|
||||||
|
|
||||||
|
@ -242,6 +242,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
|
var newPlan *deploy.Plan
|
||||||
var walkResult result.Result
|
var walkResult result.Result
|
||||||
go func() {
|
go func() {
|
||||||
opts := deploy.Options{
|
opts := deploy.Options{
|
||||||
|
@ -259,7 +260,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
|
||||||
DisableResourceReferences: deployment.Options.DisableResourceReferences,
|
DisableResourceReferences: deployment.Options.DisableResourceReferences,
|
||||||
DisableOutputValues: deployment.Options.DisableOutputValues,
|
DisableOutputValues: deployment.Options.DisableOutputValues,
|
||||||
}
|
}
|
||||||
walkResult = deployment.Deployment.Execute(ctx, opts, preview)
|
newPlan, walkResult = deployment.Deployment.Execute(ctx, opts, preview)
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -290,7 +291,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
|
||||||
// Emit a summary event.
|
// Emit a summary event.
|
||||||
deployment.Options.Events.summaryEvent(preview, actions.MaybeCorrupt(), duration, changes, policyPacks)
|
deployment.Options.Events.summaryEvent(preview, actions.MaybeCorrupt(), duration, changes, policyPacks)
|
||||||
|
|
||||||
return changes, res
|
return newPlan, changes, res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (deployment *deployment) Close() error {
|
func (deployment *deployment) Close() error {
|
||||||
|
|
|
@ -23,7 +23,12 @@ import (
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
"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(u != nil, "u")
|
||||||
contract.Require(ctx != nil, "ctx")
|
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)
|
info, err := newDeploymentContext(u, "destroy", ctx.ParentSpan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer info.Close()
|
defer info.Close()
|
||||||
|
|
||||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer emitter.Close()
|
defer emitter.Close()
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Import(u UpdateInfo, ctx *Context, opts UpdateOptions, imports []deploy.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(u != nil, "u")
|
||||||
contract.Require(ctx != nil, "ctx")
|
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)
|
info, err := newDeploymentContext(u, "import", ctx.ParentSpan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer info.Close()
|
defer info.Close()
|
||||||
|
|
||||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer emitter.Close()
|
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
|
// Run the initial update. The import should fail due to a mismatch in inputs between the program and the
|
||||||
// actual resource state.
|
// actual resource state.
|
||||||
project := p.GetProject()
|
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)
|
assert.NotNil(t, res)
|
||||||
|
|
||||||
// Run a second update after fixing the inputs. The import should succeed.
|
// Run a second update after fixing the inputs. The import should succeed.
|
||||||
inputs["foo"] = resource.NewStringProperty("bar")
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
switch urn := entry.Step.URN(); urn {
|
||||||
|
@ -107,7 +107,7 @@ func TestImportOption(t *testing.T) {
|
||||||
assert.Len(t, snap.Resources, 2)
|
assert.Len(t, snap.Resources, 2)
|
||||||
|
|
||||||
// Now, run another update. The update should succeed and there should be no diffs.
|
// 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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
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.
|
// Change a property value and run a third update. The update should succeed.
|
||||||
inputs["foo"] = resource.NewStringProperty("rab")
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
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.
|
// Change the property value s.t. the resource requires replacement. The update should fail.
|
||||||
inputs["foo"] = resource.NewStringProperty("replace")
|
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)
|
assert.NotNil(t, res)
|
||||||
|
|
||||||
// Finally, destroy the stack. The `Delete` function should be called.
|
// 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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
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.
|
// 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")
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
switch urn := entry.Step.URN(); urn {
|
||||||
|
@ -182,7 +182,7 @@ func TestImportOption(t *testing.T) {
|
||||||
importID = r.ID
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
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
|
// Then set the import ID and run another update. The update should succeed and should show an import-replace and
|
||||||
// a delete-replaced.
|
// a delete-replaced.
|
||||||
importID = "id"
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
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.
|
// Change the program to read a resource rather than creating one.
|
||||||
readID = "id"
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
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.
|
// Now have the program import the resource. We should see an import-replace and a read-discard.
|
||||||
readID, importID = "", readID
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
switch urn := entry.Step.URN(); urn {
|
||||||
|
@ -327,7 +327,7 @@ func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
|
||||||
|
|
||||||
// Run the initial update. The import should succeed.
|
// Run the initial update. The import should succeed.
|
||||||
project := p.GetProject()
|
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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
switch urn := entry.Step.URN(); urn {
|
||||||
|
@ -345,7 +345,7 @@ func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
|
||||||
assert.Len(t, snap.Resources, 2)
|
assert.Len(t, snap.Resources, 2)
|
||||||
|
|
||||||
// Now, run another update. The update should succeed and there should be no diffs.
|
// 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 {
|
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
switch urn := entry.Step.URN(); urn {
|
switch urn := entry.Step.URN(); urn {
|
||||||
|
@ -483,7 +483,7 @@ func TestImportPlan(t *testing.T) {
|
||||||
|
|
||||||
// Run the initial update.
|
// Run the initial update.
|
||||||
project := p.GetProject()
|
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.Nil(t, res)
|
||||||
|
|
||||||
// Run an import.
|
// Run an import.
|
||||||
|
@ -491,7 +491,7 @@ func TestImportPlan(t *testing.T) {
|
||||||
Type: "pkgA:m:typA",
|
Type: "pkgA:m:typA",
|
||||||
Name: "resB",
|
Name: "resB",
|
||||||
ID: "imported-id",
|
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.Nil(t, res)
|
||||||
assert.Len(t, snap.Resources, 4)
|
assert.Len(t, snap.Resources, 4)
|
||||||
|
@ -552,7 +552,7 @@ func TestImportIgnoreChanges(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
project := p.GetProject()
|
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.Nil(t, res)
|
||||||
|
|
||||||
assert.Len(t, snap.Resources, 2)
|
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,
|
Parallel: 1,
|
||||||
Host: deploytest.NewPluginHost(nil, nil, nil, loaders...),
|
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,
|
validate := func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
||||||
_ []Event, res result.Result) result.Result {
|
_ []Event, res result.Result) result.Result {
|
||||||
|
|
||||||
|
|
|
@ -39,16 +39,25 @@ func (u *updateInfo) GetTarget() *deploy.Target {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ImportOp(imports []deploy.Import) TestOp {
|
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)
|
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,
|
type ValidateFunc func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
||||||
events []Event, res result.Result) result.Result
|
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,
|
func (op TestOp) Run(project workspace.Project, target deploy.Target, opts UpdateOptions,
|
||||||
dryRun bool, backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
|
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,
|
target deploy.Target, opts UpdateOptions, dryRun bool,
|
||||||
backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
|
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.
|
// Create an appropriate update info and context.
|
||||||
info := &updateInfo{project: project, target: target}
|
info := &updateInfo{project: project, target: target}
|
||||||
|
|
||||||
|
@ -93,21 +111,21 @@ func (op TestOp) RunWithContext(
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Run the step and its validator.
|
// Run the step and its validator.
|
||||||
_, res := op(info, ctx, opts, dryRun)
|
plan, _, res := op(info, ctx, opts, dryRun)
|
||||||
contract.IgnoreClose(journal)
|
contract.IgnoreClose(journal)
|
||||||
|
|
||||||
if validate != nil {
|
if validate != nil {
|
||||||
res = validate(project, target, journal.Entries(), firedEvents, res)
|
res = validate(project, target, journal.Entries(), firedEvents, res)
|
||||||
}
|
}
|
||||||
if dryRun {
|
if dryRun {
|
||||||
return nil, res
|
return plan, nil, res
|
||||||
}
|
}
|
||||||
|
|
||||||
snap := journal.Snap(target.Snapshot)
|
snap := journal.Snap(target.Snapshot)
|
||||||
if res == nil && snap != nil {
|
if res == nil && snap != nil {
|
||||||
res = result.WrapIfNonNil(snap.VerifyIntegrity())
|
res = result.WrapIfNonNil(snap.VerifyIntegrity())
|
||||||
}
|
}
|
||||||
return snap, res
|
return nil, snap, res
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestStep struct {
|
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()
|
stack, _, _ := p.getNames()
|
||||||
|
|
||||||
cfg := p.Config
|
cfg := p.Config
|
||||||
|
@ -180,7 +198,10 @@ func (p *TestPlan) GetTarget(snapshot *deploy.Snapshot) deploy.Target {
|
||||||
Name: stack,
|
Name: stack,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Decrypter: p.Decrypter,
|
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)
|
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.
|
// 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 {
|
func CloneSnapshot(t *testing.T, snap *deploy.Snapshot) *deploy.Snapshot {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
@ -204,12 +237,9 @@ func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot
|
||||||
project := p.GetProject()
|
project := p.GetProject()
|
||||||
snap := snapshot
|
snap := snapshot
|
||||||
for _, step := range p.Steps {
|
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 {
|
if !step.SkipPreview {
|
||||||
previewSnap := CloneSnapshot(t, snap)
|
previewSnap := CloneSnapshot(t, snap)
|
||||||
previewTarget := p.GetTarget(previewSnap)
|
previewTarget := p.GetTarget(t, previewSnap)
|
||||||
// Don't run validate on the preview step
|
// Don't run validate on the preview step
|
||||||
_, res := step.Op.Run(project, previewTarget, p.Options, true, p.BackendClient, nil)
|
_, res := step.Op.Run(project, previewTarget, p.Options, true, p.BackendClient, nil)
|
||||||
if step.ExpectFailure {
|
if step.ExpectFailure {
|
||||||
|
@ -221,7 +251,7 @@ func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
var res result.Result
|
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)
|
snap, res = step.Op.Run(project, target, p.Options, false, p.BackendClient, step.Validate)
|
||||||
if step.ExpectFailure {
|
if step.ExpectFailure {
|
||||||
assertIsErrorOrBailResult(t, res)
|
assertIsErrorOrBailResult(t, res)
|
||||||
|
|
|
@ -23,7 +23,12 @@ import (
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
"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(u != nil, "u")
|
||||||
contract.Require(ctx != nil, "ctx")
|
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)
|
info, err := newDeploymentContext(u, "refresh", ctx.ParentSpan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer info.Close()
|
defer info.Close()
|
||||||
|
|
||||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer emitter.Close()
|
defer emitter.Close()
|
||||||
|
|
||||||
|
|
|
@ -146,6 +146,9 @@ type UpdateOptions struct {
|
||||||
|
|
||||||
// the plugin host to use for this update
|
// the plugin host to use for this update
|
||||||
Host plugin.Host
|
Host plugin.Host
|
||||||
|
|
||||||
|
// The plan to use for the update, if any.
|
||||||
|
Plan *deploy.Plan
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceChanges contains the aggregate resource changes by operation type.
|
// ResourceChanges contains the aggregate resource changes by operation type.
|
||||||
|
@ -165,7 +168,9 @@ func (changes ResourceChanges) HasChanges() bool {
|
||||||
return c > 0
|
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(u != nil, "update")
|
||||||
contract.Require(ctx != nil, "ctx")
|
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)
|
info, err := newDeploymentContext(u, "update", ctx.ParentSpan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer info.Close()
|
defer info.Close()
|
||||||
|
|
||||||
emitter, err := makeEventEmitter(ctx.Events, u)
|
emitter, err := makeEventEmitter(ctx.Events, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer emitter.Close()
|
defer emitter.Close()
|
||||||
|
|
||||||
|
@ -420,7 +425,7 @@ func newUpdateSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(ctx *Context, info *deploymentContext, opts deploymentOptions,
|
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.
|
// Refresh and Import do not execute Policy Packs.
|
||||||
policies := map[string]string{}
|
policies := map[string]string{}
|
||||||
|
@ -445,7 +450,7 @@ func update(ctx *Context, info *deploymentContext, opts deploymentOptions,
|
||||||
|
|
||||||
deployment, err := newDeployment(ctx, info, opts, preview)
|
deployment, err := newDeployment(ctx, info, opts, preview)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result.FromError(err)
|
return nil, nil, result.FromError(err)
|
||||||
}
|
}
|
||||||
defer contract.IgnoreClose(deployment)
|
defer contract.IgnoreClose(deployment)
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/pulumi/pulumi/pkg/v3/resource/graph"
|
"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/diag"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
"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/resource/plugin"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
"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 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
|
// A running deployment emits events that indicate its progress. These events must be used to record the new state
|
||||||
// of the deployment target.
|
// of the deployment target.
|
||||||
|
@ -153,6 +184,7 @@ type Deployment struct {
|
||||||
target *Target // the deployment target.
|
target *Target // the deployment target.
|
||||||
prev *Snapshot // the old resource snapshot for comparison.
|
prev *Snapshot // the old resource snapshot for comparison.
|
||||||
olds map[resource.URN]*resource.State // a map of all old resources.
|
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.
|
imports []Import // resources to import, if this is an import deployment.
|
||||||
isImport bool // true 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.
|
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.
|
depGraph *graph.DependencyGraph // the dependency graph of the old snapshot.
|
||||||
providers *providers.Registry // the provider registry for this deployment.
|
providers *providers.Registry // the provider registry for this deployment.
|
||||||
goals *goalMap // the set of resource goals generated by the 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
|
// 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
|
// 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.
|
// 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) {
|
localPolicyPackPaths []string, preview bool, backendClient BackendClient) (*Deployment, error) {
|
||||||
|
|
||||||
contract.Assert(ctx != nil)
|
contract.Assert(ctx != nil)
|
||||||
|
@ -343,6 +376,7 @@ func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source S
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
target: target,
|
target: target,
|
||||||
prev: prev,
|
prev: prev,
|
||||||
|
plan: plan,
|
||||||
olds: olds,
|
olds: olds,
|
||||||
source: source,
|
source: source,
|
||||||
localPolicyPackPaths: localPolicyPackPaths,
|
localPolicyPackPaths: localPolicyPackPaths,
|
||||||
|
@ -351,6 +385,7 @@ func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source S
|
||||||
providers: reg,
|
providers: reg,
|
||||||
goals: newGoals,
|
goals: newGoals,
|
||||||
news: newResources,
|
news: newResources,
|
||||||
|
newPlans: newResourcePlan(target.Config),
|
||||||
}, nil
|
}, 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.
|
// 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}
|
deploymentExec := &deploymentExecutor{deployment: d}
|
||||||
return deploymentExec.Execute(ctx, opts, preview)
|
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
|
// Execute executes a deployment to completion, using the given cancellation context and running a preview
|
||||||
// or update.
|
// 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.
|
// 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
|
// 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.
|
// 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.
|
// Before doing anything else, optionally refresh each resource in the base checkpoint.
|
||||||
if opts.Refresh {
|
if opts.Refresh {
|
||||||
if res := ex.refresh(callerCtx, opts, preview); res != nil {
|
if res := ex.refresh(callerCtx, opts, preview); res != nil {
|
||||||
return res
|
return nil, res
|
||||||
}
|
}
|
||||||
if opts.RefreshOnly {
|
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)
|
replaceTargetsOpt := createTargetMap(opts.ReplaceTargets)
|
||||||
destroyTargetsOpt := createTargetMap(opts.DestroyTargets)
|
destroyTargetsOpt := createTargetMap(opts.DestroyTargets)
|
||||||
if res := ex.checkTargets(opts.ReplaceTargets, OpReplace); res != nil {
|
if res := ex.checkTargets(opts.ReplaceTargets, OpReplace); res != nil {
|
||||||
return res
|
return nil, res
|
||||||
}
|
}
|
||||||
if res := ex.checkTargets(opts.DestroyTargets, OpDelete); res != nil {
|
if res := ex.checkTargets(opts.DestroyTargets, OpDelete); res != nil {
|
||||||
return res
|
return nil, res
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateTargetsOpt != nil || replaceTargetsOpt != nil) && destroyTargetsOpt != nil {
|
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.
|
// Begin iterating the source.
|
||||||
src, res := ex.deployment.source.Iterate(callerCtx, opts, ex.deployment)
|
src, res := ex.deployment.source.Iterate(callerCtx, opts, ex.deployment)
|
||||||
if res != nil {
|
if res != nil {
|
||||||
return res
|
return nil, res
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up a step generator for this deployment.
|
// 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.
|
// Retire any pending deletes that are currently present in this deployment.
|
||||||
if res := ex.retirePendingDeletes(callerCtx, opts, preview); res != nil {
|
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
|
// 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 {
|
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 {
|
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)
|
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() {
|
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
|
// 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)
|
logging.V(4).Infof("deploymentExecutor.Execute(...): error analyzing resources: %v", resErr)
|
||||||
ex.reportError("", 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
|
// TODO(cyrusn): We seem to be losing any information about the original 'res's errors. Should
|
||||||
// we be doing a merge here?
|
// we be doing a merge here?
|
||||||
ex.reportExecResult("failed", preview)
|
ex.reportExecResult("failed", preview)
|
||||||
return result.Bail()
|
return nil, result.Bail()
|
||||||
} else if canceled {
|
} else if canceled {
|
||||||
ex.reportExecResult("canceled", preview)
|
ex.reportExecResult("canceled", preview)
|
||||||
return result.Bail()
|
return nil, result.Bail()
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return ex.deployment.newPlans.plan(), res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ex *deploymentExecutor) performDeletes(
|
func (ex *deploymentExecutor) performDeletes(
|
||||||
|
@ -375,8 +413,7 @@ func (ex *deploymentExecutor) handleSingleEvent(event SourceEvent) result.Result
|
||||||
steps, res = ex.stepGen.GenerateReadSteps(e)
|
steps, res = ex.stepGen.GenerateReadSteps(e)
|
||||||
case RegisterResourceOutputsEvent:
|
case RegisterResourceOutputsEvent:
|
||||||
logging.V(4).Infof("deploymentExecutor.handleSingleEvent(...): received register resource outputs")
|
logging.V(4).Infof("deploymentExecutor.handleSingleEvent(...): received register resource outputs")
|
||||||
ex.stepExec.ExecuteRegisterResourceOutputs(e)
|
return ex.stepExec.ExecuteRegisterResourceOutputs(e)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if res != nil {
|
if res != nil {
|
||||||
|
@ -433,9 +470,13 @@ func (ex *deploymentExecutor) retirePendingDeletes(callerCtx context.Context, op
|
||||||
}
|
}
|
||||||
|
|
||||||
// import imports a list of resources into a stack.
|
// 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 {
|
if len(ex.deployment.imports) == 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an executor for this import.
|
// Create an executor for this import.
|
||||||
|
@ -461,12 +502,12 @@ func (ex *deploymentExecutor) importResources(callerCtx context.Context, opts Op
|
||||||
} else {
|
} else {
|
||||||
ex.reportExecResult("failed", preview)
|
ex.reportExecResult("failed", preview)
|
||||||
}
|
}
|
||||||
return result.Bail()
|
return nil, result.Bail()
|
||||||
} else if canceled {
|
} else if canceled {
|
||||||
ex.reportExecResult("canceled", preview)
|
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.
|
// 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) {
|
if !assert.Error(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,7 @@ func NewImportDeployment(ctx *plugin.Context, target *Target, projectName tokens
|
||||||
source: NewErrorSource(projectName),
|
source: NewErrorSource(projectName),
|
||||||
preview: preview,
|
preview: preview,
|
||||||
providers: reg,
|
providers: reg,
|
||||||
|
newPlans: newResourcePlan(target.Config),
|
||||||
}, nil
|
}, 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
|
package deploy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/secrets"
|
"github.com/pulumi/pulumi/pkg/v3/secrets"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
"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/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
|
// 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.
|
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.
|
// 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.
|
// This property is not checked; for verification, please refer to the VerifyIntegrity function below.
|
||||||
func NewSnapshot(manifest Manifest, secretsManager secrets.Manager,
|
func NewSnapshot(manifest Manifest, secretsManager secrets.Manager,
|
||||||
|
|
|
@ -1166,6 +1166,28 @@ func (op StepOp) Suffix() string {
|
||||||
return ""
|
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.
|
// getProvider fetches the provider for the given step.
|
||||||
func getProvider(s Step) (plugin.Provider, error) {
|
func getProvider(s Step) (plugin.Provider, error) {
|
||||||
if providers.IsProviderType(s.Type()) {
|
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/resource"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
"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/logging"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -147,7 +148,7 @@ func (se *stepExecutor) ExecuteParallel(antichain antichain) completionToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteRegisterResourceOutputs services a RegisterResourceOutputsEvent synchronously on the calling goroutine.
|
// 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.
|
// Look up the final state in the pending registration list.
|
||||||
urn := e.URN()
|
urn := e.URN()
|
||||||
value, has := se.pendingNews.Load(urn)
|
value, has := se.pendingNews.Load(urn)
|
||||||
|
@ -160,7 +161,27 @@ func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputs
|
||||||
outs := e.Outputs()
|
outs := e.Outputs()
|
||||||
se.log(synchronousWorkerID,
|
se.log(synchronousWorkerID,
|
||||||
"registered resource outputs %s: old=#%d, new=#%d", urn, len(reg.New().Outputs), len(outs))
|
"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 there is an event subscription for finishing the resource, execute them.
|
||||||
if e := se.opts.Events; e != nil {
|
if e := se.opts.Events; e != nil {
|
||||||
if eventerr := e.OnResourceOutputs(reg); eventerr != 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())
|
diagMsg := diag.RawMessage(reg.URN(), outErr.Error())
|
||||||
se.deployment.Diag().Errorf(diagMsg)
|
se.deployment.Diag().Errorf(diagMsg)
|
||||||
se.cancelDueToError()
|
se.cancelDueToError()
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.Done()
|
e.Done()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errored returns whether or not this step executor saw a step whose execution ended in failure.
|
// 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 {
|
if _, hasGoal := se.deployment.goals.get(newState.URN); hasGoal {
|
||||||
se.deployment.news.set(newState.URN, newState)
|
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 {
|
if events != nil {
|
||||||
|
|
|
@ -179,6 +179,39 @@ func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, res
|
||||||
contract.Assert(len(steps) == 0)
|
contract.Assert(len(steps) == 0)
|
||||||
return nil, res
|
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() {
|
if !sg.isTargetedUpdate() {
|
||||||
return steps, nil
|
return steps, nil
|
||||||
}
|
}
|
||||||
|
@ -272,6 +305,29 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
|
||||||
inputs = processedInputs
|
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
|
// 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.
|
// get serialized into the checkpoint file.
|
||||||
new := resource.NewState(goal.Type, urn, goal.Custom, false, "", inputs, nil, goal.Parent, goal.Protect, false,
|
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
|
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
|
sg.dependentReplaceKeys[dependentResource.URN] = toReplace[i].keys
|
||||||
|
|
||||||
logging.V(7).Infof("Planner decided to delete '%v' due to dependence on condemned resource '%v'",
|
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
|
// If -target was provided to either `pulumi update` or `pulumi destroy` then only delete
|
||||||
// resources that were specified.
|
// resources that were specified.
|
||||||
allowedResourcesToDelete, res := sg.determineAllowedResourcesToDeleteFromTargets(targetsOpt)
|
allowedResourcesToDelete, res := sg.determineAllowedResourcesToDeleteFromTargets(targetsOpt)
|
||||||
|
|
|
@ -23,8 +23,6 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
||||||
"github.com/pulumi/pulumi/pkg/v3/secrets"
|
"github.com/pulumi/pulumi/pkg/v3/secrets"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
"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"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
"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/util/contract"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
||||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,23 +102,7 @@ func SerializeDeployment(snap *deploy.Snapshot, sm secrets.Manager, showSecrets
|
||||||
contract.Require(snap != nil, "snap")
|
contract.Require(snap != nil, "snap")
|
||||||
|
|
||||||
// Capture the version information into a manifest.
|
// Capture the version information into a manifest.
|
||||||
manifest := apitype.ManifestV1{
|
manifest := snap.Manifest.Serialize()
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a specific secrets manager was not provided, use the one in the snapshot, if present.
|
// If a specific secrets manager was not provided, use the one in the snapshot, if present.
|
||||||
if sm == nil {
|
if sm == nil {
|
||||||
|
@ -223,25 +204,9 @@ func DeserializeUntypedDeployment(
|
||||||
// DeserializeDeploymentV3 deserializes a typed DeploymentV3 into a `deploy.Snapshot`.
|
// DeserializeDeploymentV3 deserializes a typed DeploymentV3 into a `deploy.Snapshot`.
|
||||||
func DeserializeDeploymentV3(deployment apitype.DeploymentV3, secretsProv SecretsProvider) (*deploy.Snapshot, error) {
|
func DeserializeDeploymentV3(deployment apitype.DeploymentV3, secretsProv SecretsProvider) (*deploy.Snapshot, error) {
|
||||||
// Unpack the versions.
|
// Unpack the versions.
|
||||||
manifest := deploy.Manifest{
|
manifest, err := deploy.DeserializeManifest(deployment.Manifest)
|
||||||
Time: deployment.Manifest.Time,
|
if err != nil {
|
||||||
Magic: deployment.Manifest.Magic,
|
return nil, err
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var secretsManager secrets.Manager
|
var secretsManager secrets.Manager
|
||||||
|
@ -295,7 +260,7 @@ func DeserializeDeploymentV3(deployment apitype.DeploymentV3, secretsProv Secret
|
||||||
ops = append(ops, desop)
|
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.
|
// 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/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.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/spf13/pflag v1.0.3 // indirect
|
||||||
github.com/src-d/gcfg v1.4.0 // indirect
|
github.com/src-d/gcfg v1.4.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.2.1 // 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 {
|
func IsInternalPropertyKey(key PropertyKey) bool {
|
||||||
return strings.HasPrefix(string(key), "__")
|
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 {
|
type ValueDiff struct {
|
||||||
Old PropertyValue // the old value.
|
Old PropertyValue // the old value.
|
||||||
New PropertyValue // the new 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).
|
Array *ArrayDiff // the array's detailed diffs (only for arrays).
|
||||||
Object *ObjectDiff // the object's detailed diffs (only for objects).
|
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.
|
// IgnoreKeyFunc is the callback type for Diff's ignore option.
|
||||||
type IgnoreKeyFunc func(key PropertyKey) bool
|
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, ignoreUnknowns bool, ignoreKeys []IgnoreKeyFunc) *ObjectDiff {
|
||||||
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
|
|
||||||
adds := make(PropertyMap)
|
adds := make(PropertyMap)
|
||||||
deletes := make(PropertyMap)
|
deletes := make(PropertyMap)
|
||||||
sames := make(PropertyMap)
|
sames := make(PropertyMap)
|
||||||
|
@ -137,7 +137,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
|
||||||
|
|
||||||
// First find any updates or deletes.
|
// First find any updates or deletes.
|
||||||
for k, old := range props {
|
for k, old := range props {
|
||||||
if ignore(k) {
|
if ignore(k) || ignoreUnknowns && (old.IsComputed() || old.IsOutput()) {
|
||||||
continue
|
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 a new exists, use it; for output properties, however, ignore differences.
|
||||||
if new.IsOutput() {
|
if new.IsOutput() {
|
||||||
sames[k] = old
|
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() {
|
if !old.HasValue() {
|
||||||
adds[k] = new
|
adds[k] = new
|
||||||
} else if !new.HasValue() {
|
} else if !new.HasValue() {
|
||||||
|
@ -156,7 +156,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
|
||||||
} else {
|
} else {
|
||||||
sames[k] = old
|
sames[k] = old
|
||||||
}
|
}
|
||||||
} else if old.HasValue() {
|
} else if old.HasValue() && (!ignoreUnknowns || !old.IsComputed()) {
|
||||||
// If there was no new property, it has been deleted.
|
// If there was no new property, it has been deleted.
|
||||||
deletes[k] = old
|
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.
|
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
|
||||||
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
|
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() {
|
if v.IsArray() && other.IsArray() {
|
||||||
old := v.ArrayValue()
|
old := v.ArrayValue()
|
||||||
new := other.ArrayValue()
|
new := other.ArrayValue()
|
||||||
|
@ -204,7 +208,7 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
|
||||||
sames := make(map[int]PropertyValue)
|
sames := make(map[int]PropertyValue)
|
||||||
updates := make(map[int]ValueDiff)
|
updates := make(map[int]ValueDiff)
|
||||||
for i := 0; i < len(old) && i < len(new); i++ {
|
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
|
updates[i] = *diff
|
||||||
} else {
|
} else {
|
||||||
sames[i] = old[i]
|
sames[i] = old[i]
|
||||||
|
@ -228,7 +232,7 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
|
||||||
if v.IsObject() && other.IsObject() {
|
if v.IsObject() && other.IsObject() {
|
||||||
old := v.ObjectValue()
|
old := v.ObjectValue()
|
||||||
new := other.ObjectValue()
|
new := other.ObjectValue()
|
||||||
if diff := old.Diff(new, ignoreKeys...); diff != nil {
|
if diff := old.diff(new, ignoreUnknowns, ignoreKeys); diff != nil {
|
||||||
return &ValueDiff{
|
return &ValueDiff{
|
||||||
Old: v,
|
Old: v,
|
||||||
New: other,
|
New: other,
|
||||||
|
@ -237,21 +241,51 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
|
||||||
}
|
}
|
||||||
return nil
|
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 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 nil
|
||||||
}
|
}
|
||||||
return &ValueDiff{Old: v, New: other}
|
return &ValueDiff{Old: v, New: other}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
|
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
|
||||||
func (props PropertyMap) DeepEquals(other PropertyMap) bool {
|
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.
|
// If any in props either doesn't exist, or is of a different value, return false.
|
||||||
for _, k := range props.StableKeys() {
|
for _, k := range props.StableKeys() {
|
||||||
v := props[k]
|
v := props[k]
|
||||||
if p, has := other[k]; has {
|
if p, has := other[k]; has {
|
||||||
if !v.DeepEquals(p) {
|
if !v.deepEquals(p, includeUnknowns) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if v.HasValue() {
|
} else if v.HasValue() {
|
||||||
|
@ -269,8 +303,19 @@ func (props PropertyMap) DeepEquals(other PropertyMap) bool {
|
||||||
return true
|
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, includeUnknowns bool) bool {
|
||||||
func (v PropertyValue) DeepEquals(other PropertyValue) 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.
|
// Arrays are equal if they are both of the same size and elements are deeply equal.
|
||||||
if v.IsArray() {
|
if v.IsArray() {
|
||||||
if !other.IsArray() {
|
if !other.IsArray() {
|
||||||
|
@ -282,7 +327,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for i, elem := range va {
|
for i, elem := range va {
|
||||||
if !elem.DeepEquals(oa[i]) {
|
if !elem.deepEquals(oa[i], includeUnknowns) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,7 +354,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
||||||
}
|
}
|
||||||
vo := v.ObjectValue()
|
vo := v.ObjectValue()
|
||||||
oa := other.ObjectValue()
|
oa := other.ObjectValue()
|
||||||
return vo.DeepEquals(oa)
|
return vo.deepEquals(oa, includeUnknowns)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secret are equal if the value they wrap are equal.
|
// Secret are equal if the value they wrap are equal.
|
||||||
|
@ -320,7 +365,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
|
||||||
vs := v.SecretValue()
|
vs := v.SecretValue()
|
||||||
os := other.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.
|
// 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() {
|
if vid.IsComputed() && oid.IsComputed() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return vid.DeepEquals(oid)
|
return vid.deepEquals(oid, includeUnknowns)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outputs are equal if each of their fields is deeply equal.
|
// 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.
|
// For all other cases, primitives are equal if their values are equal.
|
||||||
return v.V == other.V
|
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
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -291,3 +293,39 @@ func (p PropertyPath) Contains(other PropertyPath) bool {
|
||||||
|
|
||||||
return true
|
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 {
|
cases := []struct {
|
||||||
path string
|
path string
|
||||||
parsed PropertyPath
|
parsed PropertyPath
|
||||||
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"root",
|
"root",
|
||||||
PropertyPath{"root"},
|
PropertyPath{"root"},
|
||||||
|
"root",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root.nested",
|
"root.nested",
|
||||||
PropertyPath{"root", "nested"},
|
PropertyPath{"root", "nested"},
|
||||||
|
"root.nested",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`root["nested"]`,
|
`root["nested"]`,
|
||||||
PropertyPath{"root", "nested"},
|
PropertyPath{"root", "nested"},
|
||||||
|
`root.nested`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root.double.nest",
|
"root.double.nest",
|
||||||
PropertyPath{"root", "double", "nest"},
|
PropertyPath{"root", "double", "nest"},
|
||||||
|
"root.double.nest",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`root["double"].nest`,
|
`root["double"].nest`,
|
||||||
PropertyPath{"root", "double", "nest"},
|
PropertyPath{"root", "double", "nest"},
|
||||||
|
`root.double.nest`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`root["double"]["nest"]`,
|
`root["double"]["nest"]`,
|
||||||
PropertyPath{"root", "double", "nest"},
|
PropertyPath{"root", "double", "nest"},
|
||||||
|
`root.double.nest`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root.array[0]",
|
"root.array[0]",
|
||||||
PropertyPath{"root", "array", 0},
|
PropertyPath{"root", "array", 0},
|
||||||
|
"root.array[0]",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root.array[1]",
|
"root.array[1]",
|
||||||
PropertyPath{"root", "array", 1},
|
PropertyPath{"root", "array", 1},
|
||||||
|
"root.array[1]",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root.array[0].nested",
|
"root.array[0].nested",
|
||||||
PropertyPath{"root", "array", 0, "nested"},
|
PropertyPath{"root", "array", 0, "nested"},
|
||||||
|
"root.array[0].nested",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root.array2[0][1].nested",
|
"root.array2[0][1].nested",
|
||||||
PropertyPath{"root", "array2", 0, 1, "nested"},
|
PropertyPath{"root", "array2", 0, 1, "nested"},
|
||||||
|
"root.array2[0][1].nested",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root.nested.array[0].double[1]",
|
"root.nested.array[0].double[1]",
|
||||||
PropertyPath{"root", "nested", "array", 0, "double", 1},
|
PropertyPath{"root", "nested", "array", 0, "double", 1},
|
||||||
|
"root.nested.array[0].double[1]",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`root["key with \"escaped\" quotes"]`,
|
`root["key with \"escaped\" quotes"]`,
|
||||||
PropertyPath{"root", `key with "escaped" quotes`},
|
PropertyPath{"root", `key with "escaped" quotes`},
|
||||||
|
`root["key with \"escaped\" quotes"]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`root["key with a ."]`,
|
`root["key with a ."]`,
|
||||||
PropertyPath{"root", "key with a ."},
|
PropertyPath{"root", "key with a ."},
|
||||||
|
`root["key with a ."]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`["root key with \"escaped\" quotes"].nested`,
|
`["root key with \"escaped\" quotes"].nested`,
|
||||||
PropertyPath{`root key with "escaped" quotes`, "nested"},
|
PropertyPath{`root key with "escaped" quotes`, "nested"},
|
||||||
|
`["root key with \"escaped\" quotes"].nested`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`["root key with a ."][1]`,
|
`["root key with a ."][1]`,
|
||||||
PropertyPath{"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)
|
parsed, err := ParsePropertyPath(c.path)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, c.parsed, parsed)
|
assert.Equal(t, c.parsed, parsed)
|
||||||
|
assert.Equal(t, c.expected, parsed.String())
|
||||||
|
|
||||||
v, ok := parsed.Get(value)
|
v, ok := parsed.Get(value)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
Loading…
Reference in a new issue