Compare commits

..

80 commits

Author SHA1 Message Date
Fraser Waters 36bf0a43dd lint 2021-11-24 23:35:26 +00:00
Fraser Waters 19dc1bf865 Fix merge conflict 2021-11-24 23:03:43 +00:00
Fraser Waters 3bad3c4abf Merge remote-tracking branch 'origin/master' into ctpp 2021-11-24 23:02:18 +00:00
Fraser Waters f0ad8cffc7 Fix up tests 2021-11-24 18:05:08 +00:00
Fraser Waters 69d81638f9 Fix unexpected deletes 2021-11-24 17:44:21 +00:00
Fraser Waters 605bc2ecdf Fix unneeded deletes 2021-11-24 17:27:27 +00:00
Fraser Waters 884d460c39 Fix reporting of unseen op errors 2021-11-24 17:10:08 +00:00
Fraser Waters 429c93def9 Add test error text 2021-11-24 16:23:05 +00:00
Fraser Waters a88200c70d Add String and GoString to Result
I got fed up assert errors in tests that looked like:
```
Expected nil, but got: &result.simpleResult{err:(*errors.fundamental)(0xc0002fa5d0)}
```

It was very hard to work out at a glance what had gone wrong and I kept
having to hook a debugger just to look at what the error was.

With GoString these now print something like:
```
Expected nil, but got: &simpleResult{err: Unexpected diag message: <{%reset%}>resource violates plan: properties changed: -zed, -baz, -foo<{%reset%}>
}
```

Which is much more ussful.
2021-11-24 15:22:58 +00:00
Fraser Waters 146680abda Start on more tests 2021-11-24 15:11:04 +00:00
Fraser Waters 6c337ad7f9 Test diag message 2021-11-24 14:22:22 +00:00
Fraser Waters 9242b835f4 Fix clone 2021-11-24 11:53:47 +00:00
Fraser Waters f9bda75d38 Actually check error 2021-11-24 10:44:20 +00:00
Fraser Waters 1cf5e7b763 Asset NoError 2021-11-24 10:37:56 +00:00
Fraser Waters 50232e26ab linting 2021-11-24 10:30:19 +00:00
Fraser Waters dfef30f5a6 Config and manifest serder 2021-11-24 10:05:44 +00:00
Fraser Waters bd7d9b797b Merge remote-tracking branch 'origin/master' into ctpp 2021-11-24 09:39:05 +00:00
Fraser Waters 8f959a03b0 wip config work 2021-11-23 16:43:27 +00:00
Fraser Waters 94bf0840df Add proper Plan type 2021-11-23 16:09:18 +00:00
Fraser Waters 37b22b8d3c Add manifest to plan 2021-11-23 15:35:34 +00:00
Fraser Waters 6022b98134 omitempty 2021-11-23 13:32:10 +00:00
Fraser Waters 202c0f0ae4 Validate outputs don't change 2021-11-23 13:26:11 +00:00
Fraser Waters 76f3e938aa Revert auto-refresh changes 2021-11-22 16:55:05 +00:00
Fraser Waters b692140160 Merge remote-tracking branch 'origin/master' into ctpp 2021-11-22 10:18:53 +00:00
Fraser Waters 5f57c2ab72 Add TestPlannedUpdateChangedStack 2021-11-19 23:23:25 +00:00
Fraser Waters ad6047cd08 Small preview plan test 2021-11-19 21:14:44 +00:00
Fraser Waters b7a5310e07 More copying in tests because I do not trust myself to get mutation correct 2021-11-19 21:12:40 +00:00
Fraser Waters e182cf0ff3 lint 2021-11-19 16:17:32 +00:00
Fraser Waters 1ddd452faa Fix TestExplicitDeleteBeforeReplace 2021-11-19 16:06:21 +00:00
Fraser Waters 7143b728b7 Fix TestGetRefreshOption 2021-11-19 14:43:33 +00:00
Fraser Waters 6d02c4adc8 Auto refresh if using plans 2021-11-19 14:29:30 +00:00
Fraser Waters b3d8cd9870
Update pkg/cmd/pulumi/preview.go
Co-authored-by: Alex Mullans <a.mullans@pulumi.com>
2021-11-19 14:24:47 +00:00
Fraser Waters 9032552d69
Update pkg/cmd/pulumi/up.go
Co-authored-by: Alex Mullans <a.mullans@pulumi.com>
2021-11-19 14:24:33 +00:00
Fraser Waters c1047fb3d9 linting 2021-11-19 14:11:12 +00:00
Fraser Waters 9739ff0eed Fix more tests 2021-11-18 12:27:22 +00:00
Fraser Waters 4fa395b03d Fix same resource test 2021-11-18 11:39:41 +00:00
Fraser Waters 598f2c7213 Resource sames test 2021-11-18 09:59:52 +00:00
Fraser Waters 96af4684db revert stack changes 2021-11-17 16:18:27 +00:00
Fraser Waters 844e8f0c1e Merge remote-tracking branch 'origin/master' into ctpp 2021-11-17 16:16:13 +00:00
Fraser Waters 20eaa27b3d fix test 2021-11-17 15:58:52 +00:00
Fraser Waters de3e95b7ab Check more constraints 2021-11-17 15:49:17 +00:00
Fraser Waters 23e9ec2c54 Fix aliased 2021-11-17 08:59:51 +00:00
Fraser Waters 3b9cf5b648 Adds/Deletes/Updates 2021-11-16 16:26:51 +00:00
Fraser Waters 40400b664b Better constraint diffs 2021-11-16 14:29:04 +00:00
Fraser Waters 878c2bb28a typo 2021-11-15 10:58:07 +00:00
Fraser Waters 83e655e19f Merge remote-tracking branch 'origin/master' into ctpp 2021-11-15 10:02:29 +00:00
Fraser Waters b1a64a65ca Hide behind envvars 2021-11-15 09:58:30 +00:00
Fraser Waters 5b6a03f548 Delete plan cmd, rename arguments to preview and up 2021-11-11 10:40:01 +00:00
Fraser Waters e2c5f12d65 Merge remote-tracking branch 'origin/master' into ctpp 2021-11-11 10:05:00 +00:00
Fraser Waters feaccb1fe1 Merge remote-tracking branch 'origin/master' into ctpp 2021-11-08 13:44:13 +00:00
Fraser Waters 9865fa58e3 Pop op before constraint check 2021-11-08 13:19:35 +00:00
Fraser Waters c41981a84f More tests 2021-11-08 12:40:10 +00:00
Fraser Waters 1db1ab5137 notes 2021-11-05 18:44:10 +00:00
Fraser Waters 82222cc19e property set shrink test 2021-11-05 14:56:49 +00:00
Fraser Waters 775ace8f13 rm Paths() 2021-11-05 08:55:07 +00:00
Fraser Waters 12c0188329 Merge remote-tracking branch 'origin/master' into ctpp 2021-11-04 21:00:51 +00:00
Fraser Waters 3e73c036fb Fix test for missing creates 2021-11-04 19:32:11 +00:00
Fraser Waters f4c28053f8 Test missing create 2021-11-04 16:52:39 +00:00
Fraser Waters d995901fa7 Test expected delete 2021-11-04 15:51:46 +00:00
Fraser Waters 505e4c5d06 Test expected deletes 2021-11-04 15:26:01 +00:00
Fraser Waters 5352638e51 lint 2021-11-04 11:54:04 +00:00
Fraser Waters fdea5bb588 Readd test_plan.go 2021-11-04 11:22:20 +00:00
Fraser Waters b5eb955301 Merge remote-tracking branch 'origin/master' into ctpp 2021-11-04 10:07:10 +00:00
Fraser Waters b1e23a5287 More tests 2021-11-03 15:56:42 +00:00
Fraser Waters 83e2cd7e4a Fix cmdutil.PrintTable to handle non-simple strings 2021-11-03 10:24:42 +00:00
Fraser Waters 8fae881a81 Fix colors 2021-11-02 22:41:17 +00:00
Fraser Waters 1b59952a20 Test and fix PropertyPath.String() 2021-11-02 15:25:36 +00:00
Fraser Waters 155c035e34 Liniting and test fixing 2021-11-02 13:52:21 +00:00
Fraser Waters 0d9043f6a2 Merge remote-tracking branch 'origin/master' into ctpp 2021-11-02 09:19:17 +00:00
Pat Gavlin 90b75141cb fixup 2021-10-29 09:07:02 -07:00
Pat Gavlin 365bfd2ef8 fix diff 2021-10-29 08:44:33 -07:00
Pat Gavlin 954bdc025b WIP: outputs in plans 2021-10-29 08:44:33 -07:00
Pat Gavlin 24a0eb0274 fixes for rebase breaks and diffs 2021-10-29 08:44:33 -07:00
Pat Gavlin 908c3aeb70 Update message 2021-10-29 08:44:31 -07:00
Pat Gavlin 6de090c540 Renames 2021-10-29 08:44:03 -07:00
Pat Gavlin 2818c9888d constraints 2021-10-29 08:43:21 -07:00
Pat Gavlin bbfd8256de plan renderer 2021-10-29 08:43:20 -07:00
Pat Gavlin 13f8d342ce Update wording 2021-10-29 08:43:20 -07:00
Pat Gavlin e101f80839 Plumb plans through the CLI. 2021-10-29 08:43:18 -07:00
Pat Gavlin 33e8980cee Implement resource plans in the engine 2021-10-29 08:42:44 -07:00
81 changed files with 2434 additions and 2020 deletions

View file

@ -26,6 +26,7 @@ import (
"github.com/pulumi/pulumi/pkg/v3/backend/display"
"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
@ -44,7 +45,7 @@ type ApplierOptions struct {
// Applier applies the changes specified by this update operation against the target stack.
type Applier func(ctx context.Context, kind apitype.UpdateKind, stack Stack, op UpdateOperation,
opts ApplierOptions, events chan<- engine.Event) (engine.ResourceChanges, result.Result)
opts ApplierOptions, events chan<- engine.Event) (*deploy.Plan, engine.ResourceChanges, result.Result)
func ActionLabel(kind apitype.UpdateKind, dryRun bool) string {
v := updateTextMap[kind]
@ -79,7 +80,7 @@ const (
)
func PreviewThenPrompt(ctx context.Context, kind apitype.UpdateKind, stack Stack,
op UpdateOperation, apply Applier) (engine.ResourceChanges, result.Result) {
op UpdateOperation, apply Applier) (*deploy.Plan, engine.ResourceChanges, result.Result) {
// create a channel to hear about the update events from the engine. this will be used so that
// we can build up the diff display in case the user asks to see the details of the diff
@ -111,22 +112,22 @@ func PreviewThenPrompt(ctx context.Context, kind apitype.UpdateKind, stack Stack
ShowLink: true,
}
changes, res := apply(ctx, kind, stack, op, opts, eventsChannel)
plan, changes, res := apply(ctx, kind, stack, op, opts, eventsChannel)
if res != nil {
close(eventsChannel)
return changes, res
return plan, changes, res
}
// If there are no changes, or we're auto-approving or just previewing, we can skip the confirmation prompt.
if op.Opts.AutoApprove || kind == apitype.PreviewUpdate {
close(eventsChannel)
return changes, nil
return plan, changes, nil
}
// Otherwise, ensure the user wants to proceed.
res = confirmBeforeUpdating(kind, stack, events, op.Opts)
close(eventsChannel)
return changes, res
return plan, changes, res
}
// confirmBeforeUpdating asks the user whether to proceed. A nil error means yes.
@ -196,10 +197,13 @@ func PreviewThenPromptThenExecute(ctx context.Context, kind apitype.UpdateKind,
// Preview the operation to the user and ask them if they want to proceed.
if !op.Opts.SkipPreview {
changes, res := PreviewThenPrompt(ctx, kind, stack, op, apply)
plan, changes, res := PreviewThenPrompt(ctx, kind, stack, op, apply)
if res != nil || kind == apitype.PreviewUpdate {
return changes, res
}
// TODO(pdg-plan): should this check for an existing plan?
op.Opts.Engine.Plan = plan
}
// Perform the change (!DryRun) and show the cloud link to the result.
@ -208,7 +212,8 @@ func PreviewThenPromptThenExecute(ctx context.Context, kind apitype.UpdateKind,
DryRun: false,
ShowLink: true,
}
return apply(ctx, kind, stack, op, opts, nil /*events*/)
_, changes, res := apply(ctx, kind, stack, op, opts, nil /*events*/)
return changes, res
}
func createDiff(updateKind apitype.UpdateKind, events []engine.Event, displayOpts display.Options) string {

View file

@ -161,7 +161,7 @@ type Backend interface {
RenameStack(ctx context.Context, stack Stack, newName tokens.QName) (StackReference, error)
// Preview shows what would be updated given the current workspace's contents.
Preview(ctx context.Context, stack Stack, op UpdateOperation) (engine.ResourceChanges, result.Result)
Preview(ctx context.Context, stack Stack, op UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result)
// Update updates the target stack with the current workspace's contents (config and code).
Update(ctx context.Context, stack Stack, op UpdateOperation) (engine.ResourceChanges, result.Result)
// Import imports resources into a stack.

View file

@ -461,12 +461,12 @@ func (b *localBackend) PackPolicies(
}
func (b *localBackend) Preview(ctx context.Context, stack backend.Stack,
op backend.UpdateOperation) (engine.ResourceChanges, result.Result) {
op backend.UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
if cmdutil.IsTruthy(os.Getenv(PulumiFilestateLockingEnvVar)) {
err := b.Lock(ctx, stack.Ref())
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer b.Unlock(ctx, stack.Ref())
}
@ -548,7 +548,7 @@ func (b *localBackend) Watch(ctx context.Context, stack backend.Stack,
func (b *localBackend) apply(
ctx context.Context, kind apitype.UpdateKind, stack backend.Stack,
op backend.UpdateOperation, opts backend.ApplierOptions,
events chan<- engine.Event) (engine.ResourceChanges, result.Result) {
events chan<- engine.Event) (*deploy.Plan, engine.ResourceChanges, result.Result) {
stackRef := stack.Ref()
stackName := stackRef.Name()
@ -563,7 +563,7 @@ func (b *localBackend) apply(
// Start the update.
update, err := b.newUpdate(stackName, op)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
// Spawn a display loop to show events on the CLI.
@ -604,19 +604,20 @@ func (b *localBackend) apply(
// Perform the update
start := time.Now().Unix()
var plan *deploy.Plan
var changes engine.ResourceChanges
var updateRes result.Result
switch kind {
case apitype.PreviewUpdate:
changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, true)
plan, changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, true)
case apitype.UpdateUpdate:
changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, opts.DryRun)
_, changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, opts.DryRun)
case apitype.ResourceImportUpdate:
changes, updateRes = engine.Import(update, engineCtx, op.Opts.Engine, op.Imports, opts.DryRun)
_, changes, updateRes = engine.Import(update, engineCtx, op.Opts.Engine, op.Imports, opts.DryRun)
case apitype.RefreshUpdate:
changes, updateRes = engine.Refresh(update, engineCtx, op.Opts.Engine, opts.DryRun)
_, changes, updateRes = engine.Refresh(update, engineCtx, op.Opts.Engine, opts.DryRun)
case apitype.DestroyUpdate:
changes, updateRes = engine.Destroy(update, engineCtx, op.Opts.Engine, opts.DryRun)
_, changes, updateRes = engine.Destroy(update, engineCtx, op.Opts.Engine, opts.DryRun)
default:
contract.Failf("Unrecognized update kind: %s", kind)
}
@ -660,16 +661,16 @@ func (b *localBackend) apply(
if updateRes != nil {
// We swallow saveErr and backupErr as they are less important than the updateErr.
return changes, updateRes
return plan, changes, updateRes
}
if saveErr != nil {
// We swallow backupErr as it is less important than the saveErr.
return changes, result.FromError(fmt.Errorf("saving update info: %w", saveErr))
return plan, changes, result.FromError(fmt.Errorf("saving update info: %w", saveErr))
}
if backupErr != nil {
return changes, result.FromError(fmt.Errorf("saving backup: %w", backupErr))
return plan, changes, result.FromError(fmt.Errorf("saving backup: %w", backupErr))
}
// Make sure to print a link to the stack's checkpoint before exiting.
@ -705,7 +706,7 @@ func (b *localBackend) apply(
}
}
return changes, nil
return plan, changes, nil
}
// query executes a query program against the resource outputs of a locally hosted stack.

View file

@ -64,7 +64,10 @@ func (s *localStack) Rename(ctx context.Context, newName tokens.QName) (backend.
return backend.RenameStack(ctx, s, newName)
}
func (s *localStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.ResourceChanges, result.Result) {
func (s *localStack) Preview(
ctx context.Context,
op backend.UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
return backend.PreviewStack(ctx, s, op)
}

View file

@ -828,7 +828,7 @@ func (b *cloudBackend) RenameStack(ctx context.Context, stack backend.Stack,
}
func (b *cloudBackend) Preview(ctx context.Context, stack backend.Stack,
op backend.UpdateOperation) (engine.ResourceChanges, result.Result) {
op backend.UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
// We can skip PreviewtThenPromptThenExecute, and just go straight to Execute.
opts := backend.ApplierOptions{
DryRun: true,
@ -931,7 +931,7 @@ func (b *cloudBackend) createAndStartUpdate(
func (b *cloudBackend) apply(
ctx context.Context, kind apitype.UpdateKind, stack backend.Stack,
op backend.UpdateOperation, opts backend.ApplierOptions,
events chan<- engine.Event) (engine.ResourceChanges, result.Result) {
events chan<- engine.Event) (*deploy.Plan, engine.ResourceChanges, result.Result) {
actionLabel := backend.ActionLabel(kind, opts.DryRun)
@ -945,7 +945,7 @@ func (b *cloudBackend) apply(
update, version, token, err :=
b.createAndStartUpdate(ctx, kind, stack, &op, opts.DryRun)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
if !op.Opts.Display.SuppressPermalink && opts.ShowLink && !op.Opts.Display.JSONDisplay {
@ -985,12 +985,12 @@ func (b *cloudBackend) query(ctx context.Context, op backend.QueryOperation,
func (b *cloudBackend) runEngineAction(
ctx context.Context, kind apitype.UpdateKind, stackRef backend.StackReference,
op backend.UpdateOperation, update client.UpdateIdentifier, token string,
callerEventsOpt chan<- engine.Event, dryRun bool) (engine.ResourceChanges, result.Result) {
callerEventsOpt chan<- engine.Event, dryRun bool) (*deploy.Plan, engine.ResourceChanges, result.Result) {
contract.Assertf(token != "", "persisted actions require a token")
u, err := b.newUpdate(ctx, stackRef, op, update, token)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
// displayEvents renders the event to the console and Pulumi service. The processor for the
@ -1039,19 +1039,20 @@ func (b *cloudBackend) runEngineAction(
engineCtx.ParentSpan = parentSpan.Context()
}
var plan *deploy.Plan
var changes engine.ResourceChanges
var res result.Result
switch kind {
case apitype.PreviewUpdate:
changes, res = engine.Update(u, engineCtx, op.Opts.Engine, true)
plan, changes, res = engine.Update(u, engineCtx, op.Opts.Engine, true)
case apitype.UpdateUpdate:
changes, res = engine.Update(u, engineCtx, op.Opts.Engine, dryRun)
_, changes, res = engine.Update(u, engineCtx, op.Opts.Engine, dryRun)
case apitype.ResourceImportUpdate:
changes, res = engine.Import(u, engineCtx, op.Opts.Engine, op.Imports, dryRun)
_, changes, res = engine.Import(u, engineCtx, op.Opts.Engine, op.Imports, dryRun)
case apitype.RefreshUpdate:
changes, res = engine.Refresh(u, engineCtx, op.Opts.Engine, dryRun)
_, changes, res = engine.Refresh(u, engineCtx, op.Opts.Engine, dryRun)
case apitype.DestroyUpdate:
changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun)
_, changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun)
default:
contract.Failf("Unrecognized update kind: %s", kind)
}
@ -1077,7 +1078,7 @@ func (b *cloudBackend) runEngineAction(
res = result.Merge(res, result.FromError(fmt.Errorf("failed to complete update: %w", completeErr)))
}
return changes, res
return plan, changes, res
}
func (b *cloudBackend) CancelCurrentUpdate(ctx context.Context, stackRef backend.StackReference) error {

View file

@ -140,7 +140,10 @@ func (s *cloudStack) Rename(ctx context.Context, newName tokens.QName) (backend.
return backend.RenameStack(ctx, s, newName)
}
func (s *cloudStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.ResourceChanges, result.Result) {
func (s *cloudStack) Preview(
ctx context.Context,
op backend.UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
return backend.PreviewStack(ctx, s, op)
}

View file

@ -57,7 +57,7 @@ type MockBackend struct {
LogoutAllF func() error
CurrentUserF func() (string, error)
PreviewF func(context.Context, Stack,
UpdateOperation) (engine.ResourceChanges, result.Result)
UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result)
UpdateF func(context.Context, Stack,
UpdateOperation) (engine.ResourceChanges, result.Result)
ImportF func(context.Context, Stack,
@ -180,7 +180,7 @@ func (be *MockBackend) GetStackCrypter(stackRef StackReference) (config.Crypter,
}
func (be *MockBackend) Preview(ctx context.Context, stack Stack,
op UpdateOperation) (engine.ResourceChanges, result.Result) {
op UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
if be.PreviewF != nil {
return be.PreviewF(ctx, stack, op)
@ -335,7 +335,7 @@ type MockStack struct {
ConfigF func() config.Map
SnapshotF func(ctx context.Context) (*deploy.Snapshot, error)
BackendF func() Backend
PreviewF func(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
PreviewF func(ctx context.Context, op UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result)
UpdateF func(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
ImportF func(ctx context.Context, op UpdateOperation,
imports []deploy.Import) (engine.ResourceChanges, result.Result)
@ -381,7 +381,10 @@ func (ms *MockStack) Backend() Backend {
panic("not implemented")
}
func (ms *MockStack) Preview(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result) {
func (ms *MockStack) Preview(
ctx context.Context,
op UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
if ms.PreviewF != nil {
return ms.PreviewF(ctx, op)
}

View file

@ -38,7 +38,7 @@ type Stack interface {
Backend() Backend // the backend this stack belongs to.
// Preview changes to this stack.
Preview(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
Preview(ctx context.Context, op UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result)
// Update this stack.
Update(ctx context.Context, op UpdateOperation) (engine.ResourceChanges, result.Result)
// Import resources into this stack.
@ -73,7 +73,11 @@ func RenameStack(ctx context.Context, s Stack, newName tokens.QName) (StackRefer
}
// PreviewStack previews changes to this stack.
func PreviewStack(ctx context.Context, s Stack, op UpdateOperation) (engine.ResourceChanges, result.Result) {
func PreviewStack(
ctx context.Context,
s Stack,
op UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result) {
return s.Backend().Preview(ctx, s, op)
}
@ -117,7 +121,10 @@ func GetStackLogs(ctx context.Context, s Stack, cfg StackConfiguration,
}
// ExportStackDeployment exports the given stack's deployment as an opaque JSON message.
func ExportStackDeployment(ctx context.Context, s Stack) (*apitype.UntypedDeployment, error) {
func ExportStackDeployment(
ctx context.Context,
s Stack) (*apitype.UntypedDeployment, error) {
return s.Backend().ExportDeployment(ctx, s)
}

View file

@ -96,7 +96,7 @@ func Watch(ctx context.Context, b Backend, stack Stack, op UpdateOperation,
op.Opts.Display.Color.Colorize(colors.SpecImportant+"Updating..."+colors.Reset+"\n"))
// Perform the update operation
_, res := apply(ctx, apitype.UpdateUpdate, stack, op, opts, nil)
_, _, res := apply(ctx, apitype.UpdateUpdate, stack, op, opts, nil)
if res != nil {
logging.V(5).Infof("watch update failed: %v", res.Error())
if res.Error() == context.Canceled {

View file

@ -38,6 +38,8 @@ func newPreviewCmd() *cobra.Command {
var configArray []string
var configPath bool
var client string
var planFilePath string
var showSecrets bool
// Flags for engine.UpdateOptions.
var jsonDisplay bool
@ -184,7 +186,7 @@ func newPreviewCmd() *cobra.Command {
Display: displayOpts,
}
changes, res := s.Preview(commandContext(), backend.UpdateOperation{
plan, changes, res := s.Preview(commandContext(), backend.UpdateOperation{
Proj: proj,
Root: root,
M: m,
@ -200,6 +202,16 @@ func newPreviewCmd() *cobra.Command {
case expectNop && changes != nil && changes.HasChanges():
return result.FromError(errors.New("error: no changes were expected but changes were proposed"))
default:
if planFilePath != "" {
encrypter, err := sm.Encrypter()
if err != nil {
return result.FromError(err)
}
if err = writePlan(planFilePath, plan, encrypter, showSecrets); err != nil {
return result.FromError(err)
}
// TODO(pdg-plan): emit a message about how to apply the plan
}
return nil
}
}),
@ -223,6 +235,13 @@ func newPreviewCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(
&configPath, "config-path", false,
"Config keys contain a path to a property in a map or list to set")
if hasExperimentalCommands() {
cmd.PersistentFlags().StringVar(
&planFilePath, "save-plan", "",
"Save the operations proposed by the preview to a plan file at the given path")
}
cmd.Flags().BoolVarP(
&showSecrets, "show-secrets", "", false, "Emit secrets in plaintext in the plan file. Defaults to `false`")
cmd.PersistentFlags().StringVar(
&client, "client", "", "The address of an existing language runtime host to connect to")

View file

@ -76,6 +76,7 @@ func newUpCmd() *cobra.Command {
var replaces []string
var targetReplaces []string
var targetDependents bool
var planFilePath string
// up implementation used when the source of the Pulumi program is in the current working directory.
upWorkingDirectory := func(opts backend.UpdateOptions) result.Result {
@ -143,6 +144,22 @@ func newUpCmd() *cobra.Command {
TargetDependents: targetDependents,
}
if planFilePath != "" {
dec, err := sm.Decrypter()
if err != nil {
return result.FromError(err)
}
enc, err := sm.Encrypter()
if err != nil {
return result.FromError(err)
}
plan, err := readPlan(planFilePath, dec, enc)
if err != nil {
return result.FromError(err)
}
opts.Engine.Plan = plan
}
changes, res := s.Update(commandContext(), backend.UpdateOperation{
Proj: proj,
Root: root,
@ -505,6 +522,14 @@ func newUpCmd() *cobra.Command {
&yes, "yes", "y", false,
"Automatically approve and perform the update after previewing it")
if hasExperimentalCommands() {
cmd.PersistentFlags().StringVar(
&planFilePath, "plan", "",
"Path to a plan file to use for the update. The update will not "+
"perform operations that exceed its plan (e.g. replacements instead of updates, or updates instead"+
"of sames).")
}
if hasDebugCommands() {
cmd.PersistentFlags().StringVar(
&eventLogPath, "event-log", "",

View file

@ -42,12 +42,15 @@ import (
"github.com/pulumi/pulumi/pkg/v3/backend/httpstate"
"github.com/pulumi/pulumi/pkg/v3/backend/state"
"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"github.com/pulumi/pulumi/pkg/v3/resource/stack"
"github.com/pulumi/pulumi/pkg/v3/secrets/passphrase"
"github.com/pulumi/pulumi/pkg/v3/util/cancel"
"github.com/pulumi/pulumi/pkg/v3/util/tracing"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/constant"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/ciutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
@ -866,6 +869,34 @@ func getRefreshOption(proj *workspace.Project, refresh string) (bool, error) {
return false, nil
}
func writePlan(path string, plan *deploy.Plan, enc config.Encrypter, showSecrets bool) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer contract.IgnoreClose(f)
deploymentPlan, err := stack.SerializePlan(plan, enc, showSecrets)
if err != nil {
return err
}
return json.NewEncoder(f).Encode(deploymentPlan)
}
func readPlan(path string, dec config.Decrypter, enc config.Encrypter) (*deploy.Plan, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer contract.IgnoreClose(f)
var deploymentPlan apitype.DeploymentPlanV1
if err := json.NewDecoder(f).Decode(&deploymentPlan); err != nil {
return nil, err
}
return stack.DeserializePlan(deploymentPlan, dec, enc)
}
func buildStackName(stackName string) (string, error) {
if strings.Count(stackName, "/") == 2 {
return stackName, nil

View file

@ -3110,10 +3110,8 @@ func generatePackageContextMap(tool string, pkg *schema.Package, goInfo GoPackag
pkg.functions = append(pkg.functions, f)
name := tokenToName(f.Token)
if pkg.names.Has(name) ||
pkg.names.Has(name+"Args") ||
pkg.names.Has(name+"Result") {
originalName := name
if pkg.names.Has(name) {
switch {
case strings.HasPrefix(name, "New"):
name = "Create" + name[3:]
@ -3126,9 +3124,15 @@ func generatePackageContextMap(tool string, pkg *schema.Package, goInfo GoPackag
if f.Inputs != nil {
pkg.names.Add(name + "Args")
if originalName != name {
pkg.renamed[originalName+"Args"] = name + "Args"
}
}
if f.Outputs != nil {
pkg.names.Add(name + "Result")
if originalName != name {
pkg.renamed[originalName+"Result"] = name + "Result"
}
}
}

View file

@ -203,11 +203,6 @@ var sdkTests = []sdkTest{
Skip: codegen.NewStringSet("python/test", "nodejs/test"),
SkipCompileCheck: codegen.NewStringSet(dotnet),
},
{
Directory: "regress-8403",
Description: "Regress pulumi/pulumi#8403",
SkipCompileCheck: codegen.NewStringSet(dotnet, python, nodejs),
},
}
var genSDKOnly bool

View file

@ -1,33 +0,0 @@
---
title: "MongoDB Atlas"
title_tag: "mongodbatlas.MongoDB Atlas"
meta_desc: ""
layout: api
no_edit_this_page: true
---
<!-- WARNING: this file was generated by test. -->
<!-- Do not edit by hand unless you're certain you know what you are doing! -->
<h2 id="resources">Resources</h2>
<ul class="api">
<li><a href="provider" title="Provider"><span class="api-symbol api-symbol--resource"></span>Provider</a></li>
</ul>
<h2 id="functions">Functions</h2>
<ul class="api">
<li><a href="getcustomdbroles" title="GetCustomDbRoles"><span class="api-symbol api-symbol--function"></span>GetCustomDbRoles</a></li>
</ul>
<h2 id="package-details">Package Details</h2>
<dl class="package-details">
<dt>Repository</dt>
<dd><a href=""></a></dd>
<dt>License</dt>
<dd></dd>
<dt>Version</dt>
<dd>0.0.1</dd>
</dl>

View file

@ -1,7 +0,0 @@
{
"emittedFiles": [
"_index.md",
"getcustomdbroles/_index.md",
"provider/_index.md"
]
}

View file

@ -1,144 +0,0 @@
---
title: "getCustomDbRoles"
title_tag: "mongodbatlas.getCustomDbRoles"
meta_desc: "Documentation for the mongodbatlas.getCustomDbRoles function with examples, input properties, output properties, and supporting types."
layout: api
no_edit_this_page: true
---
<!-- WARNING: this file was generated by test. -->
<!-- Do not edit by hand unless you're certain you know what you are doing! -->
## Using getCustomDbRoles {#using}
{{< chooser language "typescript,python,go,csharp" / >}}
{{% choosable language nodejs %}}
<div class="highlight"
><pre class="chroma"><code class="language-typescript" data-lang="typescript"
><span class="k">function </span>getCustomDbRoles<span class="p">(</span><span class="nx">args</span><span class="p">:</span> <span class="nx">GetCustomDbRolesArgs</span><span class="p">,</span> <span class="nx">opts</span><span class="p">?:</span> <span class="nx"><a href="/docs/reference/pkg/nodejs/pulumi/pulumi/#InvokeOptions">InvokeOptions</a></span><span class="p">): Promise&lt;<span class="nx"><a href="#result">GetCustomDbRolesResult</a></span>></span
></code></pre></div>
{{% /choosable %}}
{{% choosable language python %}}
<div class="highlight"><pre class="chroma"><code class="language-python" data-lang="python"
><span class="k">def </span>get_custom_db_roles<span class="p">(</span><span class="nx">opts</span><span class="p">:</span> <span class="nx"><a href="/docs/reference/pkg/python/pulumi/#pulumi.InvokeOptions">Optional[InvokeOptions]</a></span> = None<span class="p">) -&gt;</span> <span>GetCustomDbRolesResult</span
></code></pre></div>
{{% /choosable %}}
{{% choosable language go %}}
<div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"
><span class="k">func </span>LookupCustomDbRoles<span class="p">(</span><span class="nx">ctx</span><span class="p"> *</span><span class="nx"><a href="https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3/go/pulumi?tab=doc#Context">Context</a></span><span class="p">,</span> <span class="nx">args</span><span class="p"> *</span><span class="nx">LookupCustomDbRolesArgs</span><span class="p">,</span> <span class="nx">opts</span><span class="p"> ...</span><span class="nx"><a href="https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3/go/pulumi?tab=doc#InvokeOption">InvokeOption</a></span><span class="p">) (*<span class="nx"><a href="#result">LookupCustomDbRolesResult</a></span>, error)</span
></code></pre></div>
&gt; Note: This function is named `LookupCustomDbRoles` in the Go SDK.
{{% /choosable %}}
{{% choosable language csharp %}}
<div class="highlight"><pre class="chroma"><code class="language-csharp" data-lang="csharp"><span class="k">public static class </span><span class="nx">GetCustomDbRoles </span><span class="p">
{</span><span class="k">
public static </span>Task&lt;<span class="nx"><a href="#result">GetCustomDbRolesResult</a></span>> <span class="p">InvokeAsync(</span><span class="nx">GetCustomDbRolesArgs</span><span class="p"> </span><span class="nx">args<span class="p">,</span> <span class="nx"><a href="/docs/reference/pkg/dotnet/Pulumi/Pulumi.InvokeOptions.html">InvokeOptions</a></span><span class="p">? </span><span class="nx">opts = null<span class="p">)</span><span class="p">
}</span></code></pre></div>
{{% /choosable %}}
The following arguments are supported:
{{% choosable language csharp %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
{{% choosable language go %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
{{% choosable language nodejs %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
{{% choosable language python %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
## getCustomDbRoles Result {#result}
The following output properties are available:
{{% choosable language csharp %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="result_csharp">
<a href="#result_csharp" style="color: inherit; text-decoration: inherit;">Result</a>
</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#getcustomdbrolesresult">Get<wbr>Custom<wbr>Db<wbr>Roles<wbr>Result</a></span>
</dt>
<dd>{{% md %}}{{% /md %}}</dd></dl>
{{% /choosable %}}
{{% choosable language go %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="result_go">
<a href="#result_go" style="color: inherit; text-decoration: inherit;">Result</a>
</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#getcustomdbrolesresult">Get<wbr>Custom<wbr>Db<wbr>Roles<wbr>Result</a></span>
</dt>
<dd>{{% md %}}{{% /md %}}</dd></dl>
{{% /choosable %}}
{{% choosable language nodejs %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="result_nodejs">
<a href="#result_nodejs" style="color: inherit; text-decoration: inherit;">result</a>
</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#getcustomdbrolesresult">Get<wbr>Custom<wbr>Db<wbr>Roles<wbr>Result</a></span>
</dt>
<dd>{{% md %}}{{% /md %}}</dd></dl>
{{% /choosable %}}
{{% choosable language python %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="result_python">
<a href="#result_python" style="color: inherit; text-decoration: inherit;">result</a>
</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#getcustomdbrolesresult">Get<wbr>Custom<wbr>Db<wbr>Roles<wbr>Result</a></span>
</dt>
<dd>{{% md %}}{{% /md %}}</dd></dl>
{{% /choosable %}}
<h2 id="package-details">Package Details</h2>
<dl class="package-details">
<dt>Repository</dt>
<dd><a href=""></a></dd>
<dt>License</dt>
<dd></dd>
</dl>

View file

@ -1,241 +0,0 @@
---
title: "Provider"
title_tag: "mongodbatlas.Provider"
meta_desc: "Documentation for the mongodbatlas.Provider resource with examples, input properties, output properties, lookup functions, and supporting types."
layout: api
no_edit_this_page: true
---
<!-- WARNING: this file was generated by test. -->
<!-- Do not edit by hand unless you're certain you know what you are doing! -->
## Create a Provider Resource {#create}
{{< chooser language "typescript,python,go,csharp" / >}}
{{% choosable language nodejs %}}
<div class="highlight"><pre class="chroma"><code class="language-typescript" data-lang="typescript"><span class="k">new </span><span class="nx">Provider</span><span class="p">(</span><span class="nx">name</span><span class="p">:</span> <span class="nx">string</span><span class="p">,</span> <span class="nx">args</span><span class="p">?:</span> <span class="nx"><a href="#inputs">ProviderArgs</a></span><span class="p">,</span> <span class="nx">opts</span><span class="p">?:</span> <span class="nx"><a href="/docs/reference/pkg/nodejs/pulumi/pulumi/#CustomResourceOptions">CustomResourceOptions</a></span><span class="p">);</span></code></pre></div>
{{% /choosable %}}
{{% choosable language python %}}
<div class="highlight"><pre class="chroma"><code class="language-python" data-lang="python"><span class=nd>@overload</span>
<span class="k">def </span><span class="nx">Provider</span><span class="p">(</span><span class="nx">resource_name</span><span class="p">:</span> <span class="nx">str</span><span class="p">,</span>
<span class="nx">opts</span><span class="p">:</span> <span class="nx"><a href="/docs/reference/pkg/python/pulumi/#pulumi.ResourceOptions">Optional[ResourceOptions]</a></span> = None<span class="p">)</span>
<span class=nd>@overload</span>
<span class="k">def </span><span class="nx">Provider</span><span class="p">(</span><span class="nx">resource_name</span><span class="p">:</span> <span class="nx">str</span><span class="p">,</span>
<span class="nx">args</span><span class="p">:</span> <span class="nx"><a href="#inputs">Optional[ProviderArgs]</a></span> = None<span class="p">,</span>
<span class="nx">opts</span><span class="p">:</span> <span class="nx"><a href="/docs/reference/pkg/python/pulumi/#pulumi.ResourceOptions">Optional[ResourceOptions]</a></span> = None<span class="p">)</span></code></pre></div>
{{% /choosable %}}
{{% choosable language go %}}
<div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="k">func </span><span class="nx">NewProvider</span><span class="p">(</span><span class="nx">ctx</span><span class="p"> *</span><span class="nx"><a href="https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3/go/pulumi?tab=doc#Context">Context</a></span><span class="p">,</span> <span class="nx">name</span><span class="p"> </span><span class="nx">string</span><span class="p">,</span> <span class="nx">args</span><span class="p"> *</span><span class="nx"><a href="#inputs">ProviderArgs</a></span><span class="p">,</span> <span class="nx">opts</span><span class="p"> ...</span><span class="nx"><a href="https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3/go/pulumi?tab=doc#ResourceOption">ResourceOption</a></span><span class="p">) (*<span class="nx">Provider</span>, error)</span></code></pre></div>
{{% /choosable %}}
{{% choosable language csharp %}}
<div class="highlight"><pre class="chroma"><code class="language-csharp" data-lang="csharp"><span class="k">public </span><span class="nx">Provider</span><span class="p">(</span><span class="nx">string</span><span class="p"> </span><span class="nx">name<span class="p">,</span> <span class="nx"><a href="#inputs">ProviderArgs</a></span><span class="p">? </span><span class="nx">args = null<span class="p">,</span> <span class="nx"><a href="/docs/reference/pkg/dotnet/Pulumi/Pulumi.CustomResourceOptions.html">CustomResourceOptions</a></span><span class="p">? </span><span class="nx">opts = null<span class="p">)</span></code></pre></div>
{{% /choosable %}}
{{% choosable language nodejs %}}
<dl class="resources-properties"><dt
class="property-required" title="Required">
<span>name</span>
<span class="property-indicator"></span>
<span class="property-type">string</span>
</dt>
<dd>The unique name of the resource.</dd><dt
class="property-optional" title="Optional">
<span>args</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#inputs">ProviderArgs</a></span>
</dt>
<dd>The arguments to resource properties.</dd><dt
class="property-optional" title="Optional">
<span>opts</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="/docs/reference/pkg/nodejs/pulumi/pulumi/#CustomResourceOptions">CustomResourceOptions</a></span>
</dt>
<dd>Bag of options to control resource&#39;s behavior.</dd></dl>
{{% /choosable %}}
{{% choosable language python %}}
<dl class="resources-properties"><dt
class="property-required" title="Required">
<span>resource_name</span>
<span class="property-indicator"></span>
<span class="property-type">str</span>
</dt>
<dd>The unique name of the resource.</dd><dt
class="property-optional" title="Optional">
<span>args</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#inputs">ProviderArgs</a></span>
</dt>
<dd>The arguments to resource properties.</dd><dt
class="property-optional" title="Optional">
<span>opts</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="/docs/reference/pkg/python/pulumi/#pulumi.ResourceOptions">ResourceOptions</a></span>
</dt>
<dd>Bag of options to control resource&#39;s behavior.</dd></dl>
{{% /choosable %}}
{{% choosable language go %}}
<dl class="resources-properties"><dt
class="property-optional" title="Optional">
<span>ctx</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3/go/pulumi?tab=doc#Context">Context</a></span>
</dt>
<dd>Context object for the current deployment.</dd><dt
class="property-required" title="Required">
<span>name</span>
<span class="property-indicator"></span>
<span class="property-type">string</span>
</dt>
<dd>The unique name of the resource.</dd><dt
class="property-optional" title="Optional">
<span>args</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#inputs">ProviderArgs</a></span>
</dt>
<dd>The arguments to resource properties.</dd><dt
class="property-optional" title="Optional">
<span>opts</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3/go/pulumi?tab=doc#ResourceOption">ResourceOption</a></span>
</dt>
<dd>Bag of options to control resource&#39;s behavior.</dd></dl>
{{% /choosable %}}
{{% choosable language csharp %}}
<dl class="resources-properties"><dt
class="property-required" title="Required">
<span>name</span>
<span class="property-indicator"></span>
<span class="property-type">string</span>
</dt>
<dd>The unique name of the resource.</dd><dt
class="property-optional" title="Optional">
<span>args</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="#inputs">ProviderArgs</a></span>
</dt>
<dd>The arguments to resource properties.</dd><dt
class="property-optional" title="Optional">
<span>opts</span>
<span class="property-indicator"></span>
<span class="property-type"><a href="/docs/reference/pkg/dotnet/Pulumi/Pulumi.CustomResourceOptions.html">CustomResourceOptions</a></span>
</dt>
<dd>Bag of options to control resource&#39;s behavior.</dd></dl>
{{% /choosable %}}
## Provider Resource Properties {#properties}
To learn more about resource properties and how to use them, see [Inputs and Outputs]({{< relref "/docs/intro/concepts/inputs-outputs" >}}) in the Architecture and Concepts docs.
### Inputs
The Provider resource accepts the following [input]({{< relref "/docs/intro/concepts/inputs-outputs" >}}) properties:
{{% choosable language csharp %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
{{% choosable language go %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
{{% choosable language nodejs %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
{{% choosable language python %}}
<dl class="resources-properties"></dl>
{{% /choosable %}}
### Outputs
All [input](#inputs) properties are implicitly available as output properties. Additionally, the Provider resource produces the following output properties:
{{% choosable language csharp %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="id_csharp">
<a href="#id_csharp" style="color: inherit; text-decoration: inherit;">Id</a>
</span>
<span class="property-indicator"></span>
<span class="property-type">string</span>
</dt>
<dd>{{% md %}}The provider-assigned unique ID for this managed resource.{{% /md %}}</dd></dl>
{{% /choosable %}}
{{% choosable language go %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="id_go">
<a href="#id_go" style="color: inherit; text-decoration: inherit;">Id</a>
</span>
<span class="property-indicator"></span>
<span class="property-type">string</span>
</dt>
<dd>{{% md %}}The provider-assigned unique ID for this managed resource.{{% /md %}}</dd></dl>
{{% /choosable %}}
{{% choosable language nodejs %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="id_nodejs">
<a href="#id_nodejs" style="color: inherit; text-decoration: inherit;">id</a>
</span>
<span class="property-indicator"></span>
<span class="property-type">string</span>
</dt>
<dd>{{% md %}}The provider-assigned unique ID for this managed resource.{{% /md %}}</dd></dl>
{{% /choosable %}}
{{% choosable language python %}}
<dl class="resources-properties"><dt class="property-"
title="">
<span id="id_python">
<a href="#id_python" style="color: inherit; text-decoration: inherit;">id</a>
</span>
<span class="property-indicator"></span>
<span class="property-type">str</span>
</dt>
<dd>{{% md %}}The provider-assigned unique ID for this managed resource.{{% /md %}}</dd></dl>
{{% /choosable %}}
<h2 id="package-details">Package Details</h2>
<dl class="package-details">
<dt>Repository</dt>
<dd><a href=""></a></dd>
<dt>License</dt>
<dd></dd>
</dl>

View file

@ -1,38 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Pulumi.Serialization;
namespace Pulumi.Mongodbatlas
{
public static class GetCustomDbRoles
{
public static Task<GetCustomDbRolesResult> InvokeAsync(GetCustomDbRolesArgs? args = null, InvokeOptions? options = null)
=> Pulumi.Deployment.Instance.InvokeAsync<GetCustomDbRolesResult>("mongodbatlas::getCustomDbRoles", args ?? new GetCustomDbRolesArgs(), options.WithVersion());
}
public sealed class GetCustomDbRolesArgs : Pulumi.InvokeArgs
{
public GetCustomDbRolesArgs()
{
}
}
[OutputType]
public sealed class GetCustomDbRolesResult
{
public readonly Outputs.GetCustomDbRolesResult? Result;
[OutputConstructor]
private GetCustomDbRolesResult(Outputs.GetCustomDbRolesResult? result)
{
Result = result;
}
}
}

View file

@ -1,21 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Pulumi.Serialization;
namespace Pulumi.Mongodbatlas.Outputs
{
[OutputType]
public sealed class GetCustomDbRolesResult
{
[OutputConstructor]
private GetCustomDbRolesResult()
{
}
}
}

View file

@ -1,46 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Pulumi.Serialization;
namespace Pulumi.Mongodbatlas
{
[MongodbatlasResourceType("pulumi:providers:mongodbatlas")]
public partial class Provider : Pulumi.ProviderResource
{
/// <summary>
/// Create a Provider resource with the given unique name, arguments, and options.
/// </summary>
///
/// <param name="name">The unique name of the resource</param>
/// <param name="args">The arguments used to populate this resource's properties</param>
/// <param name="options">A bag of options that control this resource's behavior</param>
public Provider(string name, ProviderArgs? args = null, CustomResourceOptions? options = null)
: base("mongodbatlas", name, args ?? new ProviderArgs(), MakeResourceOptions(options, ""))
{
}
private static CustomResourceOptions MakeResourceOptions(CustomResourceOptions? options, Input<string>? id)
{
var defaultOptions = new CustomResourceOptions
{
Version = Utilities.Version,
};
var merged = CustomResourceOptions.Merge(defaultOptions, options);
// Override the ID if one was specified for consistency with other language SDKs.
merged.Id = id ?? merged.Id;
return merged;
}
}
public sealed class ProviderArgs : Pulumi.ResourceArgs
{
public ProviderArgs()
{
}
}
}

View file

@ -1,55 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Pulumi Corp.</Authors>
<Company>Pulumi Corp.</Company>
<Description></Description>
<PackageLicenseExpression></PackageLicenseExpression>
<PackageProjectUrl></PackageProjectUrl>
<RepositoryUrl></RepositoryUrl>
<PackageIcon>logo.png</PackageIcon>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable>
<UseSharedCompilation>false</UseSharedCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1701;1702;1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="version.txt" />
<None Include="version.txt" Pack="True" PackagePath="content" />
</ItemGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup>
<None Include="logo.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
</Project>

View file

@ -1,87 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
using System;
using System.IO;
using System.Reflection;
using Pulumi;
namespace Pulumi.Mongodbatlas
{
static class Utilities
{
public static string? GetEnv(params string[] names)
{
foreach (var n in names)
{
var value = Environment.GetEnvironmentVariable(n);
if (value != null)
{
return value;
}
}
return null;
}
static string[] trueValues = { "1", "t", "T", "true", "TRUE", "True" };
static string[] falseValues = { "0", "f", "F", "false", "FALSE", "False" };
public static bool? GetEnvBoolean(params string[] names)
{
var s = GetEnv(names);
if (s != null)
{
if (Array.IndexOf(trueValues, s) != -1)
{
return true;
}
if (Array.IndexOf(falseValues, s) != -1)
{
return false;
}
}
return null;
}
public static int? GetEnvInt32(params string[] names) => int.TryParse(GetEnv(names), out int v) ? (int?)v : null;
public static double? GetEnvDouble(params string[] names) => double.TryParse(GetEnv(names), out double v) ? (double?)v : null;
public static InvokeOptions WithVersion(this InvokeOptions? options)
{
if (options?.Version != null)
{
return options;
}
return new InvokeOptions
{
Parent = options?.Parent,
Provider = options?.Provider,
Version = Version,
};
}
private readonly static string version;
public static string Version => version;
static Utilities()
{
var assembly = typeof(Utilities).GetTypeInfo().Assembly;
using var stream = assembly.GetManifestResourceStream("Pulumi.Mongodbatlas.version.txt");
using var reader = new StreamReader(stream ?? throw new NotSupportedException("Missing embedded version.txt file"));
version = reader.ReadToEnd().Trim();
var parts = version.Split("\n");
if (parts.Length == 2)
{
// The first part is the provider name.
version = parts[1].Trim();
}
}
}
internal sealed class MongodbatlasResourceTypeAttribute : Pulumi.ResourceTypeAttribute
{
public MongodbatlasResourceTypeAttribute(string type) : base(type, Utilities.Version)
{
}
}
}

View file

@ -1,11 +0,0 @@
{
"emittedFiles": [
"GetCustomDbRoles.cs",
"Outputs/GetCustomDbRolesResult.cs",
"Provider.cs",
"Pulumi.Mongodbatlas.csproj",
"README.md",
"Utilities.cs",
"logo.png"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -1,10 +0,0 @@
{
"emittedFiles": [
"mongodbatlas/doc.go",
"mongodbatlas/getCustomDbRoles.go",
"mongodbatlas/init.go",
"mongodbatlas/provider.go",
"mongodbatlas/pulumiTypes.go",
"mongodbatlas/pulumiUtilities.go"
]
}

View file

@ -1,3 +0,0 @@
// Package mongodbatlas exports types, functions, subpackages for provisioning mongodbatlas resources.
//
package mongodbatlas

View file

@ -1,24 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
package mongodbatlas
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func LookupCustomDbRoles(ctx *pulumi.Context, args *LookupCustomDbRolesArgs, opts ...pulumi.InvokeOption) (*LookupCustomDbRolesResult, error) {
var rv LookupCustomDbRolesResult
err := ctx.Invoke("mongodbatlas::getCustomDbRoles", args, &rv, opts...)
if err != nil {
return nil, err
}
return &rv, nil
}
type LookupCustomDbRolesArgs struct {
}
type LookupCustomDbRolesResult struct {
Result *GetCustomDbRolesResult `pulumi:"result"`
}

View file

@ -1,40 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
package mongodbatlas
import (
"fmt"
"github.com/blang/semver"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type pkg struct {
version semver.Version
}
func (p *pkg) Version() semver.Version {
return p.version
}
func (p *pkg) ConstructProvider(ctx *pulumi.Context, name, typ, urn string) (pulumi.ProviderResource, error) {
if typ != "pulumi:providers:mongodbatlas" {
return nil, fmt.Errorf("unknown provider type: %s", typ)
}
r := &Provider{}
err := ctx.RegisterResource(typ, name, nil, r, pulumi.URN_(urn))
return r, err
}
func init() {
version, err := PkgVersion()
if err != nil {
fmt.Printf("failed to determine package version. defaulting to v1: %v\n", err)
}
pulumi.RegisterResourcePackage(
"mongodbatlas",
&pkg{version},
)
}

View file

@ -1,79 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
package mongodbatlas
import (
"context"
"reflect"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type Provider struct {
pulumi.ProviderResourceState
}
// NewProvider registers a new resource with the given unique name, arguments, and options.
func NewProvider(ctx *pulumi.Context,
name string, args *ProviderArgs, opts ...pulumi.ResourceOption) (*Provider, error) {
if args == nil {
args = &ProviderArgs{}
}
var resource Provider
err := ctx.RegisterResource("pulumi:providers:mongodbatlas", name, args, &resource, opts...)
if err != nil {
return nil, err
}
return &resource, nil
}
type providerArgs struct {
}
// The set of arguments for constructing a Provider resource.
type ProviderArgs struct {
}
func (ProviderArgs) ElementType() reflect.Type {
return reflect.TypeOf((*providerArgs)(nil)).Elem()
}
type ProviderInput interface {
pulumi.Input
ToProviderOutput() ProviderOutput
ToProviderOutputWithContext(ctx context.Context) ProviderOutput
}
func (*Provider) ElementType() reflect.Type {
return reflect.TypeOf((**Provider)(nil)).Elem()
}
func (i *Provider) ToProviderOutput() ProviderOutput {
return i.ToProviderOutputWithContext(context.Background())
}
func (i *Provider) ToProviderOutputWithContext(ctx context.Context) ProviderOutput {
return pulumi.ToOutputWithContext(ctx, i).(ProviderOutput)
}
type ProviderOutput struct{ *pulumi.OutputState }
func (ProviderOutput) ElementType() reflect.Type {
return reflect.TypeOf((**Provider)(nil)).Elem()
}
func (o ProviderOutput) ToProviderOutput() ProviderOutput {
return o
}
func (o ProviderOutput) ToProviderOutputWithContext(ctx context.Context) ProviderOutput {
return o
}
func init() {
pulumi.RegisterInputType(reflect.TypeOf((*ProviderInput)(nil)).Elem(), &Provider{})
pulumi.RegisterOutputType(ProviderOutput{})
}

View file

@ -1,136 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
package mongodbatlas
import (
"context"
"reflect"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type GetCustomDbRolesResult struct {
}
// GetCustomDbRolesResultInput is an input type that accepts GetCustomDbRolesResultArgs and GetCustomDbRolesResultOutput values.
// You can construct a concrete instance of `GetCustomDbRolesResultInput` via:
//
// GetCustomDbRolesResultArgs{...}
type GetCustomDbRolesResultInput interface {
pulumi.Input
ToGetCustomDbRolesResultOutput() GetCustomDbRolesResultOutput
ToGetCustomDbRolesResultOutputWithContext(context.Context) GetCustomDbRolesResultOutput
}
type GetCustomDbRolesResultArgs struct {
}
func (GetCustomDbRolesResultArgs) ElementType() reflect.Type {
return reflect.TypeOf((*GetCustomDbRolesResult)(nil)).Elem()
}
func (i GetCustomDbRolesResultArgs) ToGetCustomDbRolesResultOutput() GetCustomDbRolesResultOutput {
return i.ToGetCustomDbRolesResultOutputWithContext(context.Background())
}
func (i GetCustomDbRolesResultArgs) ToGetCustomDbRolesResultOutputWithContext(ctx context.Context) GetCustomDbRolesResultOutput {
return pulumi.ToOutputWithContext(ctx, i).(GetCustomDbRolesResultOutput)
}
func (i GetCustomDbRolesResultArgs) ToGetCustomDbRolesResultPtrOutput() GetCustomDbRolesResultPtrOutput {
return i.ToGetCustomDbRolesResultPtrOutputWithContext(context.Background())
}
func (i GetCustomDbRolesResultArgs) ToGetCustomDbRolesResultPtrOutputWithContext(ctx context.Context) GetCustomDbRolesResultPtrOutput {
return pulumi.ToOutputWithContext(ctx, i).(GetCustomDbRolesResultOutput).ToGetCustomDbRolesResultPtrOutputWithContext(ctx)
}
// GetCustomDbRolesResultPtrInput is an input type that accepts GetCustomDbRolesResultArgs, GetCustomDbRolesResultPtr and GetCustomDbRolesResultPtrOutput values.
// You can construct a concrete instance of `GetCustomDbRolesResultPtrInput` via:
//
// GetCustomDbRolesResultArgs{...}
//
// or:
//
// nil
type GetCustomDbRolesResultPtrInput interface {
pulumi.Input
ToGetCustomDbRolesResultPtrOutput() GetCustomDbRolesResultPtrOutput
ToGetCustomDbRolesResultPtrOutputWithContext(context.Context) GetCustomDbRolesResultPtrOutput
}
type getCustomDbRolesResultPtrType GetCustomDbRolesResultArgs
func GetCustomDbRolesResultPtr(v *GetCustomDbRolesResultArgs) GetCustomDbRolesResultPtrInput {
return (*getCustomDbRolesResultPtrType)(v)
}
func (*getCustomDbRolesResultPtrType) ElementType() reflect.Type {
return reflect.TypeOf((**GetCustomDbRolesResult)(nil)).Elem()
}
func (i *getCustomDbRolesResultPtrType) ToGetCustomDbRolesResultPtrOutput() GetCustomDbRolesResultPtrOutput {
return i.ToGetCustomDbRolesResultPtrOutputWithContext(context.Background())
}
func (i *getCustomDbRolesResultPtrType) ToGetCustomDbRolesResultPtrOutputWithContext(ctx context.Context) GetCustomDbRolesResultPtrOutput {
return pulumi.ToOutputWithContext(ctx, i).(GetCustomDbRolesResultPtrOutput)
}
type GetCustomDbRolesResultOutput struct{ *pulumi.OutputState }
func (GetCustomDbRolesResultOutput) ElementType() reflect.Type {
return reflect.TypeOf((*GetCustomDbRolesResult)(nil)).Elem()
}
func (o GetCustomDbRolesResultOutput) ToGetCustomDbRolesResultOutput() GetCustomDbRolesResultOutput {
return o
}
func (o GetCustomDbRolesResultOutput) ToGetCustomDbRolesResultOutputWithContext(ctx context.Context) GetCustomDbRolesResultOutput {
return o
}
func (o GetCustomDbRolesResultOutput) ToGetCustomDbRolesResultPtrOutput() GetCustomDbRolesResultPtrOutput {
return o.ToGetCustomDbRolesResultPtrOutputWithContext(context.Background())
}
func (o GetCustomDbRolesResultOutput) ToGetCustomDbRolesResultPtrOutputWithContext(ctx context.Context) GetCustomDbRolesResultPtrOutput {
return o.ApplyTWithContext(ctx, func(_ context.Context, v GetCustomDbRolesResult) *GetCustomDbRolesResult {
return &v
}).(GetCustomDbRolesResultPtrOutput)
}
type GetCustomDbRolesResultPtrOutput struct{ *pulumi.OutputState }
func (GetCustomDbRolesResultPtrOutput) ElementType() reflect.Type {
return reflect.TypeOf((**GetCustomDbRolesResult)(nil)).Elem()
}
func (o GetCustomDbRolesResultPtrOutput) ToGetCustomDbRolesResultPtrOutput() GetCustomDbRolesResultPtrOutput {
return o
}
func (o GetCustomDbRolesResultPtrOutput) ToGetCustomDbRolesResultPtrOutputWithContext(ctx context.Context) GetCustomDbRolesResultPtrOutput {
return o
}
func (o GetCustomDbRolesResultPtrOutput) Elem() GetCustomDbRolesResultOutput {
return o.ApplyT(func(v *GetCustomDbRolesResult) GetCustomDbRolesResult {
if v != nil {
return *v
}
var ret GetCustomDbRolesResult
return ret
}).(GetCustomDbRolesResultOutput)
}
func init() {
pulumi.RegisterInputType(reflect.TypeOf((*GetCustomDbRolesResultInput)(nil)).Elem(), GetCustomDbRolesResultArgs{})
pulumi.RegisterInputType(reflect.TypeOf((*GetCustomDbRolesResultPtrInput)(nil)).Elem(), GetCustomDbRolesResultArgs{})
pulumi.RegisterOutputType(GetCustomDbRolesResultOutput{})
pulumi.RegisterOutputType(GetCustomDbRolesResultPtrOutput{})
}

View file

@ -1,85 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
package mongodbatlas
import (
"fmt"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/blang/semver"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type envParser func(v string) interface{}
func parseEnvBool(v string) interface{} {
b, err := strconv.ParseBool(v)
if err != nil {
return nil
}
return b
}
func parseEnvInt(v string) interface{} {
i, err := strconv.ParseInt(v, 0, 0)
if err != nil {
return nil
}
return int(i)
}
func parseEnvFloat(v string) interface{} {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil
}
return f
}
func parseEnvStringArray(v string) interface{} {
var result pulumi.StringArray
for _, item := range strings.Split(v, ";") {
result = append(result, pulumi.String(item))
}
return result
}
func getEnvOrDefault(def interface{}, parser envParser, vars ...string) interface{} {
for _, v := range vars {
if value := os.Getenv(v); value != "" {
if parser != nil {
return parser(value)
}
return value
}
}
return def
}
// PkgVersion uses reflection to determine the version of the current package.
func PkgVersion() (semver.Version, error) {
type sentinal struct{}
pkgPath := reflect.TypeOf(sentinal{}).PkgPath()
re := regexp.MustCompile("^.*/pulumi-mongodbatlas/sdk(/v\\d+)?")
if match := re.FindStringSubmatch(pkgPath); match != nil {
vStr := match[1]
if len(vStr) == 0 { // If the version capture group was empty, default to v1.
return semver.Version{Major: 1}, nil
}
return semver.MustParse(fmt.Sprintf("%s.0.0", vStr[2:])), nil
}
return semver.Version{}, fmt.Errorf("failed to determine the package version from %s", pkgPath)
}
// isZero is a null safe check for if a value is it's types zero value.
func isZero(v interface{}) bool {
if v == nil {
return true
}
return reflect.ValueOf(v).IsZero()
}

View file

@ -1,14 +0,0 @@
{
"emittedFiles": [
"README.md",
"getCustomDbRoles.ts",
"index.ts",
"package.json",
"provider.ts",
"tsconfig.json",
"types/index.ts",
"types/input.ts",
"types/output.ts",
"utilities.ts"
]
}

View file

@ -1,26 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
import * as pulumi from "@pulumi/pulumi";
import { input as inputs, output as outputs } from "./types";
import * as utilities from "./utilities";
export function getCustomDbRoles(args?: GetCustomDbRolesArgs, opts?: pulumi.InvokeOptions): Promise<GetCustomDbRolesResult> {
args = args || {};
if (!opts) {
opts = {}
}
if (!opts.version) {
opts.version = utilities.getVersion();
}
return pulumi.runtime.invoke("mongodbatlas::getCustomDbRoles", {
}, opts);
}
export interface GetCustomDbRolesArgs {
}
export interface GetCustomDbRolesResult {
readonly result?: outputs.GetCustomDbRolesResult;
}

View file

@ -1,28 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
import * as pulumi from "@pulumi/pulumi";
import * as utilities from "./utilities";
// Export members:
export * from "./getCustomDbRoles";
export * from "./provider";
// Export sub-modules:
import * as types from "./types";
export {
types,
};
import { Provider } from "./provider";
pulumi.runtime.registerResourcePackage("mongodbatlas", {
version: utilities.getVersion(),
constructProvider: (name: string, type: string, urn: string): pulumi.ProviderResource => {
if (type !== "pulumi:providers:mongodbatlas") {
throw new Error(`unknown provider type ${type}`);
}
return new Provider(name, <any>undefined, { urn });
},
});

View file

@ -1,16 +0,0 @@
{
"name": "@pulumi/mongodbatlas",
"version": "${VERSION}",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^4.3.5"
},
"peerDependencies": {
"@pulumi/pulumi": "latest"
},
"pulumi": {
"resource": true
}
}

View file

@ -1,46 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
import * as pulumi from "@pulumi/pulumi";
import * as utilities from "./utilities";
export class Provider extends pulumi.ProviderResource {
/** @internal */
public static readonly __pulumiType = 'mongodbatlas';
/**
* Returns true if the given object is an instance of Provider. This is designed to work even
* when multiple copies of the Pulumi SDK have been loaded into the same process.
*/
public static isInstance(obj: any): obj is Provider {
if (obj === undefined || obj === null) {
return false;
}
return obj['__pulumiType'] === Provider.__pulumiType;
}
/**
* Create a Provider resource with the given unique name, arguments, and options.
*
* @param name The _unique_ name of the resource.
* @param args The arguments to use to populate this resource's properties.
* @param opts A bag of options that control this resource's behavior.
*/
constructor(name: string, args?: ProviderArgs, opts?: pulumi.ResourceOptions) {
let resourceInputs: pulumi.Inputs = {};
opts = opts || {};
{
}
if (!opts.version) {
opts = pulumi.mergeOptions(opts, { version: utilities.getVersion()});
}
super(Provider.__pulumiType, name, resourceInputs, opts);
}
}
/**
* The set of arguments for constructing a Provider resource.
*/
export interface ProviderArgs {
}

View file

@ -1,24 +0,0 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"strict": true
},
"files": [
"getCustomDbRoles.ts",
"index.ts",
"provider.ts",
"types/index.ts",
"types/input.ts",
"types/output.ts",
"utilities.ts"
]
}

View file

@ -1,11 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
// Export sub-modules:
import * as input from "./input";
import * as output from "./output";
export {
input,
output,
};

View file

@ -1,6 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
import * as pulumi from "@pulumi/pulumi";
import { input as inputs, output as outputs } from "../types";

View file

@ -1,9 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
import * as pulumi from "@pulumi/pulumi";
import { input as inputs, output as outputs } from "../types";
export interface GetCustomDbRolesResult {
}

View file

@ -1,49 +0,0 @@
// *** WARNING: this file was generated by test. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
export function getEnv(...vars: string[]): string | undefined {
for (const v of vars) {
const value = process.env[v];
if (value) {
return value;
}
}
return undefined;
}
export function getEnvBoolean(...vars: string[]): boolean | undefined {
const s = getEnv(...vars);
if (s !== undefined) {
// NOTE: these values are taken from https://golang.org/src/strconv/atob.go?s=351:391#L1, which is what
// Terraform uses internally when parsing boolean values.
if (["1", "t", "T", "true", "TRUE", "True"].find(v => v === s) !== undefined) {
return true;
}
if (["0", "f", "F", "false", "FALSE", "False"].find(v => v === s) !== undefined) {
return false;
}
}
return undefined;
}
export function getEnvNumber(...vars: string[]): number | undefined {
const s = getEnv(...vars);
if (s !== undefined) {
const f = parseFloat(s);
if (!isNaN(f)) {
return f;
}
}
return undefined;
}
export function getVersion(): string {
let version = require('./package.json').version;
// Node allows for the version to be prefixed by a "v", while semver doesn't.
// If there is a v, strip it off.
if (version.indexOf('v') === 0) {
version = version.slice(1);
}
return version;
}

View file

@ -1,12 +0,0 @@
{
"emittedFiles": [
"pulumi_mongodbatlas/README.md",
"pulumi_mongodbatlas/__init__.py",
"pulumi_mongodbatlas/_utilities.py",
"pulumi_mongodbatlas/get_custom_db_roles.py",
"pulumi_mongodbatlas/outputs.py",
"pulumi_mongodbatlas/provider.py",
"pulumi_mongodbatlas/py.typed",
"setup.py"
]
}

View file

@ -1,25 +0,0 @@
# coding=utf-8
# *** WARNING: this file was generated by test. ***
# *** Do not edit by hand unless you're certain you know what you are doing! ***
from . import _utilities
import typing
# Export this package's modules as members:
from .get_custom_db_roles import *
from .provider import *
from . import outputs
_utilities.register(
resource_modules="""
[]
""",
resource_packages="""
[
{
"pkg": "mongodbatlas",
"token": "pulumi:providers:mongodbatlas",
"fqn": "pulumi_mongodbatlas",
"class": "Provider"
}
]
"""
)

View file

@ -1,236 +0,0 @@
# coding=utf-8
# *** WARNING: this file was generated by test. ***
# *** Do not edit by hand unless you're certain you know what you are doing! ***
import importlib.util
import inspect
import json
import os
import pkg_resources
import sys
import typing
import pulumi
import pulumi.runtime
from semver import VersionInfo as SemverVersion
from parver import Version as PEP440Version
def get_env(*args):
for v in args:
value = os.getenv(v)
if value is not None:
return value
return None
def get_env_bool(*args):
str = get_env(*args)
if str is not None:
# NOTE: these values are taken from https://golang.org/src/strconv/atob.go?s=351:391#L1, which is what
# Terraform uses internally when parsing boolean values.
if str in ["1", "t", "T", "true", "TRUE", "True"]:
return True
if str in ["0", "f", "F", "false", "FALSE", "False"]:
return False
return None
def get_env_int(*args):
str = get_env(*args)
if str is not None:
try:
return int(str)
except:
return None
return None
def get_env_float(*args):
str = get_env(*args)
if str is not None:
try:
return float(str)
except:
return None
return None
def _get_semver_version():
# __name__ is set to the fully-qualified name of the current module, In our case, it will be
# <some module>._utilities. <some module> is the module we want to query the version for.
root_package, *rest = __name__.split('.')
# pkg_resources uses setuptools to inspect the set of installed packages. We use it here to ask
# for the currently installed version of the root package (i.e. us) and get its version.
# Unfortunately, PEP440 and semver differ slightly in incompatible ways. The Pulumi engine expects
# to receive a valid semver string when receiving requests from the language host, so it's our
# responsibility as the library to convert our own PEP440 version into a valid semver string.
pep440_version_string = pkg_resources.require(root_package)[0].version
pep440_version = PEP440Version.parse(pep440_version_string)
(major, minor, patch) = pep440_version.release
prerelease = None
if pep440_version.pre_tag == 'a':
prerelease = f"alpha.{pep440_version.pre}"
elif pep440_version.pre_tag == 'b':
prerelease = f"beta.{pep440_version.pre}"
elif pep440_version.pre_tag == 'rc':
prerelease = f"rc.{pep440_version.pre}"
elif pep440_version.dev is not None:
prerelease = f"dev.{pep440_version.dev}"
# The only significant difference between PEP440 and semver as it pertains to us is that PEP440 has explicit support
# for dev builds, while semver encodes them as "prerelease" versions. In order to bridge between the two, we convert
# our dev build version into a prerelease tag. This matches what all of our other packages do when constructing
# their own semver string.
return SemverVersion(major=major, minor=minor, patch=patch, prerelease=prerelease)
# Determine the version once and cache the value, which measurably improves program performance.
_version = _get_semver_version()
_version_str = str(_version)
def get_version():
return _version_str
def get_resource_args_opts(resource_args_type, resource_options_type, *args, **kwargs):
"""
Return the resource args and options given the *args and **kwargs of a resource's
__init__ method.
"""
resource_args, opts = None, None
# If the first item is the resource args type, save it and remove it from the args list.
if args and isinstance(args[0], resource_args_type):
resource_args, args = args[0], args[1:]
# Now look at the first item in the args list again.
# If the first item is the resource options class, save it.
if args and isinstance(args[0], resource_options_type):
opts = args[0]
# If resource_args is None, see if "args" is in kwargs, and, if so, if it's typed as the
# the resource args type.
if resource_args is None:
a = kwargs.get("args")
if isinstance(a, resource_args_type):
resource_args = a
# If opts is None, look it up in kwargs.
if opts is None:
opts = kwargs.get("opts")
return resource_args, opts
# Temporary: just use pulumi._utils.lazy_import once everyone upgrades.
def lazy_import(fullname):
import pulumi._utils as u
f = getattr(u, 'lazy_import', None)
if f is None:
f = _lazy_import_temp
return f(fullname)
# Copied from pulumi._utils.lazy_import, see comments there.
def _lazy_import_temp(fullname):
m = sys.modules.get(fullname, None)
if m is not None:
return m
spec = importlib.util.find_spec(fullname)
m = sys.modules.get(fullname, None)
if m is not None:
return m
loader = importlib.util.LazyLoader(spec.loader)
spec.loader = loader
module = importlib.util.module_from_spec(spec)
m = sys.modules.get(fullname, None)
if m is not None:
return m
sys.modules[fullname] = module
loader.exec_module(module)
return module
class Package(pulumi.runtime.ResourcePackage):
def __init__(self, pkg_info):
super().__init__()
self.pkg_info = pkg_info
def version(self):
return _version
def construct_provider(self, name: str, typ: str, urn: str) -> pulumi.ProviderResource:
if typ != self.pkg_info['token']:
raise Exception(f"unknown provider type {typ}")
Provider = getattr(lazy_import(self.pkg_info['fqn']), self.pkg_info['class'])
return Provider(name, pulumi.ResourceOptions(urn=urn))
class Module(pulumi.runtime.ResourceModule):
def __init__(self, mod_info):
super().__init__()
self.mod_info = mod_info
def version(self):
return _version
def construct(self, name: str, typ: str, urn: str) -> pulumi.Resource:
class_name = self.mod_info['classes'].get(typ, None)
if class_name is None:
raise Exception(f"unknown resource type {typ}")
TheClass = getattr(lazy_import(self.mod_info['fqn']), class_name)
return TheClass(name, pulumi.ResourceOptions(urn=urn))
def register(resource_modules, resource_packages):
resource_modules = json.loads(resource_modules)
resource_packages = json.loads(resource_packages)
for pkg_info in resource_packages:
pulumi.runtime.register_resource_package(pkg_info['pkg'], Package(pkg_info))
for mod_info in resource_modules:
pulumi.runtime.register_resource_module(
mod_info['pkg'],
mod_info['mod'],
Module(mod_info))
_F = typing.TypeVar('_F', bound=typing.Callable[..., typing.Any])
def lift_output_func(func: typing.Any) -> typing.Callable[[_F], _F]:
"""Decorator internally used on {fn}_output lifted function versions
to implement them automatically from the un-lifted function."""
func_sig = inspect.signature(func)
def lifted_func(*args, opts=None, **kwargs):
bound_args = func_sig.bind(*args, **kwargs)
# Convert tuple to list, see pulumi/pulumi#8172
args_list = list(bound_args.args)
return pulumi.Output.from_input({
'args': args_list,
'kwargs': bound_args.kwargs
}).apply(lambda resolved_args: func(*resolved_args['args'],
opts=opts,
**resolved_args['kwargs']))
return (lambda _: lifted_func)

View file

@ -1,52 +0,0 @@
# coding=utf-8
# *** WARNING: this file was generated by test. ***
# *** Do not edit by hand unless you're certain you know what you are doing! ***
import warnings
import pulumi
import pulumi.runtime
from typing import Any, Mapping, Optional, Sequence, Union, overload
from . import _utilities
from . import outputs
__all__ = [
'GetCustomDbRolesResult',
'AwaitableGetCustomDbRolesResult',
'get_custom_db_roles',
]
@pulumi.output_type
class GetCustomDbRolesResult:
def __init__(__self__, result=None):
if result and not isinstance(result, dict):
raise TypeError("Expected argument 'result' to be a dict")
pulumi.set(__self__, "result", result)
@property
@pulumi.getter
def result(self) -> Optional['outputs.GetCustomDbRolesResult']:
return pulumi.get(self, "result")
class AwaitableGetCustomDbRolesResult(GetCustomDbRolesResult):
# pylint: disable=using-constant-test
def __await__(self):
if False:
yield self
return GetCustomDbRolesResult(
result=self.result)
def get_custom_db_roles(opts: Optional[pulumi.InvokeOptions] = None) -> AwaitableGetCustomDbRolesResult:
"""
Use this data source to access information about an existing resource.
"""
__args__ = dict()
if opts is None:
opts = pulumi.InvokeOptions()
if opts.version is None:
opts.version = _utilities.get_version()
__ret__ = pulumi.runtime.invoke('mongodbatlas::getCustomDbRoles', __args__, opts=opts, typ=GetCustomDbRolesResult).value
return AwaitableGetCustomDbRolesResult(
result=__ret__.result)

View file

@ -1,20 +0,0 @@
# coding=utf-8
# *** WARNING: this file was generated by test. ***
# *** Do not edit by hand unless you're certain you know what you are doing! ***
import warnings
import pulumi
import pulumi.runtime
from typing import Any, Mapping, Optional, Sequence, Union, overload
from . import _utilities
__all__ = [
'GetCustomDbRolesResult',
]
@pulumi.output_type
class GetCustomDbRolesResult(dict):
def __init__(__self__):
pass

View file

@ -1,73 +0,0 @@
# coding=utf-8
# *** WARNING: this file was generated by test. ***
# *** Do not edit by hand unless you're certain you know what you are doing! ***
import warnings
import pulumi
import pulumi.runtime
from typing import Any, Mapping, Optional, Sequence, Union, overload
from . import _utilities
__all__ = ['ProviderArgs', 'Provider']
@pulumi.input_type
class ProviderArgs:
def __init__(__self__):
"""
The set of arguments for constructing a Provider resource.
"""
pass
class Provider(pulumi.ProviderResource):
@overload
def __init__(__self__,
resource_name: str,
opts: Optional[pulumi.ResourceOptions] = None,
__props__=None):
"""
Create a Mongodbatlas resource with the given unique name, props, and options.
:param str resource_name: The name of the resource.
:param pulumi.ResourceOptions opts: Options for the resource.
"""
...
@overload
def __init__(__self__,
resource_name: str,
args: Optional[ProviderArgs] = None,
opts: Optional[pulumi.ResourceOptions] = None):
"""
Create a Mongodbatlas resource with the given unique name, props, and options.
:param str resource_name: The name of the resource.
:param ProviderArgs args: The arguments to use to populate this resource's properties.
:param pulumi.ResourceOptions opts: Options for the resource.
"""
...
def __init__(__self__, resource_name: str, *args, **kwargs):
resource_args, opts = _utilities.get_resource_args_opts(ProviderArgs, pulumi.ResourceOptions, *args, **kwargs)
if resource_args is not None:
__self__._internal_init(resource_name, opts, **resource_args.__dict__)
else:
__self__._internal_init(resource_name, *args, **kwargs)
def _internal_init(__self__,
resource_name: str,
opts: Optional[pulumi.ResourceOptions] = None,
__props__=None):
if opts is None:
opts = pulumi.ResourceOptions()
if not isinstance(opts, pulumi.ResourceOptions):
raise TypeError('Expected resource options to be a ResourceOptions instance')
if opts.version is None:
opts.version = _utilities.get_version()
if opts.id is None:
if __props__ is not None:
raise TypeError('__props__ is only valid when passed in combination with a valid opts.id to get an existing resource')
__props__ = ProviderArgs.__new__(ProviderArgs)
super(Provider, __self__).__init__(
'mongodbatlas',
resource_name,
__props__,
opts)

View file

@ -1,58 +0,0 @@
# coding=utf-8
# *** WARNING: this file was generated by test. ***
# *** Do not edit by hand unless you're certain you know what you are doing! ***
import errno
from setuptools import setup, find_packages
from setuptools.command.install import install
from subprocess import check_call
VERSION = "0.0.0"
PLUGIN_VERSION = "0.0.0"
class InstallPluginCommand(install):
def run(self):
install.run(self)
try:
check_call(['pulumi', 'plugin', 'install', 'resource', 'mongodbatlas', PLUGIN_VERSION])
except OSError as error:
if error.errno == errno.ENOENT:
print(f"""
There was an error installing the mongodbatlas resource provider plugin.
It looks like `pulumi` is not installed on your system.
Please visit https://pulumi.com/ to install the Pulumi CLI.
You may try manually installing the plugin by running
`pulumi plugin install resource mongodbatlas {PLUGIN_VERSION}`
""")
else:
raise
def readme():
try:
with open('README.md', encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
return "mongodbatlas Pulumi Package - Development Version"
setup(name='pulumi_mongodbatlas',
version=VERSION,
long_description=readme(),
long_description_content_type='text/markdown',
cmdclass={
'install': InstallPluginCommand,
},
packages=find_packages(),
package_data={
'pulumi_mongodbatlas': [
'py.typed',
]
},
install_requires=[
'parver>=0.2.1',
'pulumi',
'semver>=2.8.1'
],
zip_safe=False)

View file

@ -1,22 +0,0 @@
{
"version": "0.0.1",
"name": "mongodbatlas",
"types": {
"mongodbatlas::getCustomDbRolesResult": {
"type": "object"
}
},
"functions": {
"mongodbatlas::getCustomDbRoles": {
"inputs": {},
"outputs": {
"type": "object",
"properties": {
"result": {
"$ref": "#/types/mongodbatlas::getCustomDbRolesResult"
}
}
}
}
}
}

View file

@ -167,7 +167,7 @@ func newDeployment(ctx *Context, info *deploymentContext, opts deploymentOptions
var depl *deploy.Deployment
if !opts.isImport {
depl, err = deploy.NewDeployment(
plugctx, target, target.Snapshot, source, localPolicyPackPaths, dryRun, ctx.BackendClient)
plugctx, target, target.Snapshot, opts.Plan, source, localPolicyPackPaths, dryRun, ctx.BackendClient)
} else {
_, defaultProviderVersions, pluginErr := installPlugins(proj, pwd, main, target, plugctx,
false /*returnInstallErrors*/)
@ -218,12 +218,12 @@ type runActions interface {
// run executes the deployment. It is primarily responsible for handling cancellation.
func (deployment *deployment) run(cancelCtx *Context, actions runActions, policyPacks map[string]string,
preview bool) (ResourceChanges, result.Result) {
preview bool) (*deploy.Plan, ResourceChanges, result.Result) {
// Change into the plugin context's working directory.
chdir, err := fsutil.Chdir(deployment.Plugctx.Pwd)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer chdir()
@ -242,6 +242,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
start := time.Now()
done := make(chan bool)
var newPlan *deploy.Plan
var walkResult result.Result
go func() {
opts := deploy.Options{
@ -259,7 +260,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
DisableResourceReferences: deployment.Options.DisableResourceReferences,
DisableOutputValues: deployment.Options.DisableOutputValues,
}
walkResult = deployment.Deployment.Execute(ctx, opts, preview)
newPlan, walkResult = deployment.Deployment.Execute(ctx, opts, preview)
close(done)
}()
@ -290,7 +291,7 @@ func (deployment *deployment) run(cancelCtx *Context, actions runActions, policy
// Emit a summary event.
deployment.Options.Events.summaryEvent(preview, actions.MaybeCorrupt(), duration, changes, policyPacks)
return changes, res
return newPlan, changes, res
}
func (deployment *deployment) Close() error {

View file

@ -23,7 +23,12 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
func Destroy(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
func Destroy(
u UpdateInfo,
ctx *Context,
opts UpdateOptions,
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
contract.Require(u != nil, "u")
contract.Require(ctx != nil, "ctx")
@ -31,13 +36,13 @@ func Destroy(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resou
info, err := newDeploymentContext(u, "destroy", ctx.ParentSpan)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer info.Close()
emitter, err := makeEventEmitter(ctx.Events, u)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer emitter.Close()

View file

@ -21,7 +21,7 @@ import (
)
func Import(u UpdateInfo, ctx *Context, opts UpdateOptions, imports []deploy.Import,
dryRun bool) (ResourceChanges, result.Result) {
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
contract.Require(u != nil, "u")
contract.Require(ctx != nil, "ctx")
@ -30,13 +30,13 @@ func Import(u UpdateInfo, ctx *Context, opts UpdateOptions, imports []deploy.Imp
info, err := newDeploymentContext(u, "import", ctx.ParentSpan)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer info.Close()
emitter, err := makeEventEmitter(ctx.Events, u)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer emitter.Close()

View file

@ -84,12 +84,12 @@ func TestImportOption(t *testing.T) {
// Run the initial update. The import should fail due to a mismatch in inputs between the program and the
// actual resource state.
project := p.GetProject()
_, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
_, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
assert.NotNil(t, res)
// Run a second update after fixing the inputs. The import should succeed.
inputs["foo"] = resource.NewStringProperty("bar")
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -107,7 +107,7 @@ func TestImportOption(t *testing.T) {
assert.Len(t, snap.Resources, 2)
// Now, run another update. The update should succeed and there should be no diffs.
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -123,7 +123,7 @@ func TestImportOption(t *testing.T) {
// Change a property value and run a third update. The update should succeed.
inputs["foo"] = resource.NewStringProperty("rab")
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -141,11 +141,11 @@ func TestImportOption(t *testing.T) {
// Change the property value s.t. the resource requires replacement. The update should fail.
inputs["foo"] = resource.NewStringProperty("replace")
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient, nil)
_, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)
assert.NotNil(t, res)
// Finally, destroy the stack. The `Delete` function should be called.
_, res = TestOp(Destroy).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
_, res = TestOp(Destroy).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -161,7 +161,7 @@ func TestImportOption(t *testing.T) {
// Now clear the ID to import and run an initial update to create a resource that we will import-replace.
importID, inputs["foo"] = "", resource.NewStringProperty("bar")
snap, res = TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
snap, res = TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -182,7 +182,7 @@ func TestImportOption(t *testing.T) {
importID = r.ID
}
}
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -199,7 +199,7 @@ func TestImportOption(t *testing.T) {
// Then set the import ID and run another update. The update should succeed and should show an import-replace and
// a delete-replaced.
importID = "id"
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
_, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -222,7 +222,7 @@ func TestImportOption(t *testing.T) {
// Change the program to read a resource rather than creating one.
readID = "id"
snap, res = TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
snap, res = TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -241,7 +241,7 @@ func TestImportOption(t *testing.T) {
// Now have the program import the resource. We should see an import-replace and a read-discard.
readID, importID = "", readID
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
_, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -327,7 +327,7 @@ func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
// Run the initial update. The import should succeed.
project := p.GetProject()
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient,
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -345,7 +345,7 @@ func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
assert.Len(t, snap.Resources, 2)
// Now, run another update. The update should succeed and there should be no diffs.
snap, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient,
snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
@ -483,7 +483,7 @@ func TestImportPlan(t *testing.T) {
// Run the initial update.
project := p.GetProject()
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
assert.Nil(t, res)
// Run an import.
@ -491,7 +491,7 @@ func TestImportPlan(t *testing.T) {
Type: "pkgA:m:typA",
Name: "resB",
ID: "imported-id",
}}).Run(project, p.GetTarget(snap), p.Options, false, p.BackendClient, nil)
}}).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)
assert.Nil(t, res)
assert.Len(t, snap.Resources, 4)
@ -552,7 +552,7 @@ func TestImportIgnoreChanges(t *testing.T) {
}
project := p.GetProject()
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
assert.Nil(t, res)
assert.Len(t, snap.Resources, 2)

File diff suppressed because it is too large Load diff

View file

@ -678,7 +678,7 @@ func TestCanceledRefresh(t *testing.T) {
Parallel: 1,
Host: deploytest.NewPluginHost(nil, nil, nil, loaders...),
}
project, target := p.GetProject(), p.GetTarget(old)
project, target := p.GetProject(), p.GetTarget(t, old)
validate := func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {

View file

@ -39,16 +39,25 @@ func (u *updateInfo) GetTarget() *deploy.Target {
}
func ImportOp(imports []deploy.Import) TestOp {
return TestOp(func(info UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
return TestOp(func(info UpdateInfo, ctx *Context, opts UpdateOptions,
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
return Import(info, ctx, opts, imports, dryRun)
})
}
type TestOp func(UpdateInfo, *Context, UpdateOptions, bool) (ResourceChanges, result.Result)
type TestOp func(UpdateInfo, *Context, UpdateOptions, bool) (*deploy.Plan, ResourceChanges, result.Result)
type ValidateFunc func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result
func (op TestOp) Plan(project workspace.Project, target deploy.Target, opts UpdateOptions,
backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Plan, result.Result) {
plan, _, res := op.runWithContext(context.Background(), project, target, opts, true, backendClient, validate)
return plan, res
}
func (op TestOp) Run(project workspace.Project, target deploy.Target, opts UpdateOptions,
dryRun bool, backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
@ -60,6 +69,15 @@ func (op TestOp) RunWithContext(
target deploy.Target, opts UpdateOptions, dryRun bool,
backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
_, snap, res := op.runWithContext(callerCtx, project, target, opts, dryRun, backendClient, validate)
return snap, res
}
func (op TestOp) runWithContext(
callerCtx context.Context, project workspace.Project,
target deploy.Target, opts UpdateOptions, dryRun bool,
backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Plan, *deploy.Snapshot, result.Result) {
// Create an appropriate update info and context.
info := &updateInfo{project: project, target: target}
@ -93,21 +111,21 @@ func (op TestOp) RunWithContext(
}()
// Run the step and its validator.
_, res := op(info, ctx, opts, dryRun)
plan, _, res := op(info, ctx, opts, dryRun)
contract.IgnoreClose(journal)
if validate != nil {
res = validate(project, target, journal.Entries(), firedEvents, res)
}
if dryRun {
return nil, res
return plan, nil, res
}
snap := journal.Snap(target.Snapshot)
if res == nil && snap != nil {
res = result.WrapIfNonNil(snap.VerifyIntegrity())
}
return snap, res
return nil, snap, res
}
type TestStep struct {
@ -168,7 +186,7 @@ func (p *TestPlan) GetProject() workspace.Project {
}
}
func (p *TestPlan) GetTarget(snapshot *deploy.Snapshot) deploy.Target {
func (p *TestPlan) GetTarget(t *testing.T, snapshot *deploy.Snapshot) deploy.Target {
stack, _, _ := p.getNames()
cfg := p.Config
@ -180,7 +198,10 @@ func (p *TestPlan) GetTarget(snapshot *deploy.Snapshot) deploy.Target {
Name: stack,
Config: cfg,
Decrypter: p.Decrypter,
Snapshot: snapshot,
// note: it's really important that the preview and update operate on different snapshots. the engine can and
// does mutate the snapshot in-place, even in previews, and sharing a snapshot between preview and update can
// cause state changes from the preview to persist even when doing an update.
Snapshot: CloneSnapshot(t, snapshot),
}
}
@ -188,6 +209,18 @@ func assertIsErrorOrBailResult(t *testing.T, res result.Result) {
assert.NotNil(t, res)
}
// ClonePlan makes a deep copy of the given plan and returns a pointer to the clone.
func ClonePlan(t *testing.T, plan *deploy.Plan) *deploy.Plan {
t.Helper()
if plan != nil {
copiedPlan := copystructure.Must(copystructure.Copy(plan)).(*deploy.Plan)
assert.True(t, reflect.DeepEqual(plan, copiedPlan))
return copiedPlan
}
return plan
}
// CloneSnapshot makes a deep copy of the given snapshot and returns a pointer to the clone.
func CloneSnapshot(t *testing.T, snap *deploy.Snapshot) *deploy.Snapshot {
t.Helper()
@ -204,12 +237,9 @@ func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot
project := p.GetProject()
snap := snapshot
for _, step := range p.Steps {
// note: it's really important that the preview and update operate on different snapshots. the engine can and
// does mutate the snapshot in-place, even in previews, and sharing a snapshot between preview and update can
// cause state changes from the preview to persist even when doing an update.
if !step.SkipPreview {
previewSnap := CloneSnapshot(t, snap)
previewTarget := p.GetTarget(previewSnap)
previewTarget := p.GetTarget(t, previewSnap)
// Don't run validate on the preview step
_, res := step.Op.Run(project, previewTarget, p.Options, true, p.BackendClient, nil)
if step.ExpectFailure {
@ -221,7 +251,7 @@ func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot
}
var res result.Result
target := p.GetTarget(snap)
target := p.GetTarget(t, snap)
snap, res = step.Op.Run(project, target, p.Options, false, p.BackendClient, step.Validate)
if step.ExpectFailure {
assertIsErrorOrBailResult(t, res)

View file

@ -23,7 +23,12 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
func Refresh(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
func Refresh(
u UpdateInfo,
ctx *Context,
opts UpdateOptions,
dryRun bool) (*deploy.Plan, ResourceChanges, result.Result) {
contract.Require(u != nil, "u")
contract.Require(ctx != nil, "ctx")
@ -31,13 +36,13 @@ func Refresh(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resou
info, err := newDeploymentContext(u, "refresh", ctx.ParentSpan)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer info.Close()
emitter, err := makeEventEmitter(ctx.Events, u)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer emitter.Close()

View file

@ -146,6 +146,9 @@ type UpdateOptions struct {
// the plugin host to use for this update
Host plugin.Host
// The plan to use for the update, if any.
Plan *deploy.Plan
}
// ResourceChanges contains the aggregate resource changes by operation type.
@ -165,7 +168,9 @@ func (changes ResourceChanges) HasChanges() bool {
return c > 0
}
func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (ResourceChanges, result.Result) {
func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (
*deploy.Plan, ResourceChanges, result.Result) {
contract.Require(u != nil, "update")
contract.Require(ctx != nil, "ctx")
@ -173,13 +178,13 @@ func Update(u UpdateInfo, ctx *Context, opts UpdateOptions, dryRun bool) (Resour
info, err := newDeploymentContext(u, "update", ctx.ParentSpan)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer info.Close()
emitter, err := makeEventEmitter(ctx.Events, u)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer emitter.Close()
@ -420,7 +425,7 @@ func newUpdateSource(
}
func update(ctx *Context, info *deploymentContext, opts deploymentOptions,
preview bool) (ResourceChanges, result.Result) {
preview bool) (*deploy.Plan, ResourceChanges, result.Result) {
// Refresh and Import do not execute Policy Packs.
policies := map[string]string{}
@ -445,7 +450,7 @@ func update(ctx *Context, info *deploymentContext, opts deploymentOptions,
deployment, err := newDeployment(ctx, info, opts, preview)
if err != nil {
return nil, result.FromError(err)
return nil, nil, result.FromError(err)
}
defer contract.IgnoreClose(deployment)

View file

@ -28,6 +28,7 @@ import (
"github.com/pulumi/pulumi/pkg/v3/resource/graph"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
@ -145,6 +146,36 @@ func (m *resourceMap) mapRange(callback func(urn resource.URN, state *resource.S
})
}
type resourcePlans struct {
m sync.RWMutex
plans Plan
}
func newResourcePlan(config config.Map) *resourcePlans {
return &resourcePlans{
plans: NewPlan(config),
}
}
func (m *resourcePlans) set(urn resource.URN, plan *ResourcePlan) {
m.m.Lock()
defer m.m.Unlock()
m.plans.ResourcePlans[urn] = plan
}
func (m *resourcePlans) get(urn resource.URN) (*ResourcePlan, bool) {
m.m.RLock()
defer m.m.RUnlock()
p, ok := m.plans.ResourcePlans[urn]
return p, ok
}
func (m *resourcePlans) plan() *Plan {
return &m.plans
}
// A Deployment manages the iterative computation and execution of a deployment based on a stream of goal states.
// A running deployment emits events that indicate its progress. These events must be used to record the new state
// of the deployment target.
@ -153,6 +184,7 @@ type Deployment struct {
target *Target // the deployment target.
prev *Snapshot // the old resource snapshot for comparison.
olds map[resource.URN]*resource.State // a map of all old resources.
plan *Plan // a map of all planned resource changes, if any.
imports []Import // resources to import, if this is an import deployment.
isImport bool // true if this is an import deployment.
schemaLoader schema.Loader // the schema cache for this deployment, if any.
@ -162,7 +194,8 @@ type Deployment struct {
depGraph *graph.DependencyGraph // the dependency graph of the old snapshot.
providers *providers.Registry // the provider registry for this deployment.
goals *goalMap // the set of resource goals generated by the deployment.
news *resourceMap // the set of new resources generated by the deployment.
news *resourceMap // the set of new resources generated by the deployment
newPlans *resourcePlans // the set of new resource plans.
}
// addDefaultProviders adds any necessary default provider definitions and references to the given snapshot. Version
@ -299,7 +332,7 @@ func buildResourceMap(prev *Snapshot, preview bool) ([]*resource.State, map[reso
//
// Note that a deployment uses internal concurrency and parallelism in various ways, so it must be closed if for some
// reason it isn't carried out to its final conclusion. This will result in cancellation and reclamation of resources.
func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source Source,
func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, plan *Plan, source Source,
localPolicyPackPaths []string, preview bool, backendClient BackendClient) (*Deployment, error) {
contract.Assert(ctx != nil)
@ -343,6 +376,7 @@ func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source S
ctx: ctx,
target: target,
prev: prev,
plan: plan,
olds: olds,
source: source,
localPolicyPackPaths: localPolicyPackPaths,
@ -351,6 +385,7 @@ func NewDeployment(ctx *plugin.Context, target *Target, prev *Snapshot, source S
providers: reg,
goals: newGoals,
news: newResources,
newPlans: newResourcePlan(target.Config),
}, nil
}
@ -405,7 +440,7 @@ func (d *Deployment) generateEventURN(event SourceEvent) resource.URN {
}
// Execute executes a deployment to completion, using the given cancellation context and running a preview or update.
func (d *Deployment) Execute(ctx context.Context, opts Options, preview bool) result.Result {
func (d *Deployment) Execute(ctx context.Context, opts Options, preview bool) (*Plan, result.Result) {
deploymentExec := &deploymentExecutor{deployment: d}
return deploymentExec.Execute(ctx, opts, preview)
}

View file

@ -114,7 +114,7 @@ func (ex *deploymentExecutor) reportError(urn resource.URN, err error) {
// Execute executes a deployment to completion, using the given cancellation context and running a preview
// or update.
func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, preview bool) result.Result {
func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, preview bool) (*Plan, result.Result) {
// Set up a goroutine that will signal cancellation to the deployment's plugins if the caller context is cancelled.
// We do not hang this off of the context we create below because we do not want the failure of a single step to
// cause other steps to fail.
@ -141,10 +141,10 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
// Before doing anything else, optionally refresh each resource in the base checkpoint.
if opts.Refresh {
if res := ex.refresh(callerCtx, opts, preview); res != nil {
return res
return nil, res
}
if opts.RefreshOnly {
return nil
return nil, nil
}
}
@ -156,10 +156,10 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
replaceTargetsOpt := createTargetMap(opts.ReplaceTargets)
destroyTargetsOpt := createTargetMap(opts.DestroyTargets)
if res := ex.checkTargets(opts.ReplaceTargets, OpReplace); res != nil {
return res
return nil, res
}
if res := ex.checkTargets(opts.DestroyTargets, OpDelete); res != nil {
return res
return nil, res
}
if (updateTargetsOpt != nil || replaceTargetsOpt != nil) && destroyTargetsOpt != nil {
@ -169,7 +169,7 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
// Begin iterating the source.
src, res := ex.deployment.source.Iterate(callerCtx, opts, ex.deployment)
if res != nil {
return res
return nil, res
}
// Set up a step generator for this deployment.
@ -177,7 +177,7 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
// Retire any pending deletes that are currently present in this deployment.
if res := ex.retirePendingDeletes(callerCtx, opts, preview); res != nil {
return res
return nil, res
}
// Derive a cancellable context for this deployment. We will only cancel this context if some piece of the
@ -236,7 +236,15 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
}
if event.Event == nil {
return false, ex.performDeletes(ctx, updateTargetsOpt, destroyTargetsOpt)
res := ex.performDeletes(ctx, updateTargetsOpt, destroyTargetsOpt)
if res != nil {
if resErr := res.Error(); resErr != nil {
logging.V(4).Infof("deploymentExecutor.Execute(...): error performing deletes: %v", resErr)
ex.reportError("", resErr)
return false, result.Bail()
}
}
return false, res
}
if res := ex.handleSingleEvent(event.Event); res != nil {
@ -267,8 +275,38 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
res = ex.checkTargets(opts.UpdateTargets, OpUpdate)
}
// Check that we did operations for everything expected in the plan. We mutate ResourcePlan.Ops as we run
// so by the time we get here everything in the map should have an empty ops list (except for unneeded deletes)
if res == nil && ex.deployment.plan != nil {
for urn, resourcePlan := range ex.deployment.plan.ResourcePlans {
if len(resourcePlan.Ops) != 0 {
if len(resourcePlan.Ops) == 1 && resourcePlan.Ops[0] == OpDelete {
// We haven't done a delete for this resource check if it was in the snapshot,
// if it's already gone this wasn't done because it wasn't needed
found := false
for i := range ex.deployment.prev.Resources {
if ex.deployment.prev.Resources[i].URN == urn {
found = true
break
}
}
// Didn't find the resource in the old snapshot so this was just an unneeded delete
if !found {
continue
}
}
err := fmt.Errorf("expected resource operations for %v but none were seen", urn)
logging.V(4).Infof("deploymentExecutor.Execute(...): error handling event: %v", err)
ex.reportError(urn, err)
res = result.Bail()
}
}
}
if res != nil && res.IsBail() {
return res
return nil, res
}
// If the step generator and step executor were both successful, then we send all the resources
@ -280,7 +318,7 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
logging.V(4).Infof("deploymentExecutor.Execute(...): error analyzing resources: %v", resErr)
ex.reportError("", resErr)
}
return result.Bail()
return nil, result.Bail()
}
}
@ -289,13 +327,13 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
// TODO(cyrusn): We seem to be losing any information about the original 'res's errors. Should
// we be doing a merge here?
ex.reportExecResult("failed", preview)
return result.Bail()
return nil, result.Bail()
} else if canceled {
ex.reportExecResult("canceled", preview)
return result.Bail()
return nil, result.Bail()
}
return res
return ex.deployment.newPlans.plan(), res
}
func (ex *deploymentExecutor) performDeletes(
@ -375,8 +413,7 @@ func (ex *deploymentExecutor) handleSingleEvent(event SourceEvent) result.Result
steps, res = ex.stepGen.GenerateReadSteps(e)
case RegisterResourceOutputsEvent:
logging.V(4).Infof("deploymentExecutor.handleSingleEvent(...): received register resource outputs")
ex.stepExec.ExecuteRegisterResourceOutputs(e)
return nil
return ex.stepExec.ExecuteRegisterResourceOutputs(e)
}
if res != nil {
@ -433,9 +470,13 @@ func (ex *deploymentExecutor) retirePendingDeletes(callerCtx context.Context, op
}
// import imports a list of resources into a stack.
func (ex *deploymentExecutor) importResources(callerCtx context.Context, opts Options, preview bool) result.Result {
func (ex *deploymentExecutor) importResources(
callerCtx context.Context,
opts Options,
preview bool) (*Plan, result.Result) {
if len(ex.deployment.imports) == 0 {
return nil
return nil, nil
}
// Create an executor for this import.
@ -461,12 +502,12 @@ func (ex *deploymentExecutor) importResources(callerCtx context.Context, opts Op
} else {
ex.reportExecResult("failed", preview)
}
return result.Bail()
return nil, result.Bail()
} else if canceled {
ex.reportExecResult("canceled", preview)
return result.Bail()
return nil, result.Bail()
}
return nil
return ex.deployment.newPlans.plan(), nil
}
// refresh refreshes the state of the base checkpoint file for the current deployment in memory.

View file

@ -42,7 +42,7 @@ func TestPendingOperationsDeployment(t *testing.T) {
},
})
_, err := NewDeployment(&plugin.Context{}, &Target{}, snap, &fixedSource{}, nil, false, nil)
_, err := NewDeployment(&plugin.Context{}, &Target{}, snap, nil, &fixedSource{}, nil, false, nil)
if !assert.Error(t, err) {
t.FailNow()
}

View file

@ -97,6 +97,7 @@ func NewImportDeployment(ctx *plugin.Context, target *Target, projectName tokens
source: NewErrorSource(projectName),
preview: preview,
providers: reg,
newPlans: newResourcePlan(target.Config),
}, nil
}

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

View file

@ -15,15 +15,12 @@
package deploy
import (
"crypto/sha256"
"fmt"
"time"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/v3/secrets"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
// Snapshot is a view of a collection of resources in an stack at a point in time. It describes resources; their
@ -36,23 +33,6 @@ type Snapshot struct {
PendingOperations []resource.Operation // all currently pending resource operations.
}
// Manifest captures versions for all binaries used to construct this snapshot.
type Manifest struct {
Time time.Time // the time this snapshot was taken.
Magic string // a magic cookie.
Version string // the pulumi command version.
Plugins []workspace.PluginInfo // the plugin versions also loaded.
}
// NewMagic creates a magic cookie out of a manifest; this can be used to check for tampering. This ignores
// any existing magic value already stored on the manifest.
func (m Manifest) NewMagic() string {
if m.Version == "" {
return ""
}
return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Version)))
}
// NewSnapshot creates a snapshot from the given arguments. The resources must be in topologically sorted order.
// This property is not checked; for verification, please refer to the VerifyIntegrity function below.
func NewSnapshot(manifest Manifest, secretsManager secrets.Manager,

View file

@ -1166,6 +1166,28 @@ func (op StepOp) Suffix() string {
return ""
}
// ConstrainedTo returns true if this operation is no more impactful than the constraint.
func (op StepOp) ConstrainedTo(constraint StepOp) bool {
var allowed []StepOp
switch constraint {
case OpSame, OpDelete, OpRead, OpReadReplacement, OpRefresh, OpReadDiscard, OpDiscardReplaced,
OpRemovePendingReplace, OpImport, OpImportReplacement:
allowed = []StepOp{constraint}
case OpCreate:
allowed = []StepOp{OpSame, OpCreate}
case OpUpdate:
allowed = []StepOp{OpSame, OpUpdate}
case OpReplace, OpCreateReplacement, OpDeleteReplaced:
allowed = []StepOp{OpSame, OpUpdate, constraint}
}
for _, candidate := range allowed {
if candidate == op {
return true
}
}
return false
}
// getProvider fetches the provider for the given step.
func getProvider(s Step) (plugin.Provider, error) {
if providers.IsProviderType(s.Type()) {

View file

@ -25,6 +25,7 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
)
const (
@ -147,7 +148,7 @@ func (se *stepExecutor) ExecuteParallel(antichain antichain) completionToken {
}
// ExecuteRegisterResourceOutputs services a RegisterResourceOutputsEvent synchronously on the calling goroutine.
func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputsEvent) {
func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputsEvent) result.Result {
// Look up the final state in the pending registration list.
urn := e.URN()
value, has := se.pendingNews.Load(urn)
@ -160,7 +161,27 @@ func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputs
outs := e.Outputs()
se.log(synchronousWorkerID,
"registered resource outputs %s: old=#%d, new=#%d", urn, len(reg.New().Outputs), len(outs))
reg.New().Outputs = e.Outputs()
reg.New().Outputs = outs
// If a plan is present check that these outputs match what we recorded before
if se.deployment.plan != nil {
resourcePlan, ok := se.deployment.plan.ResourcePlans[urn]
if !ok {
return result.FromError(fmt.Errorf("no plan for resource %v", urn))
}
if diffs, has := resourcePlan.Outputs.DiffIncludeUnknowns(outs); has {
return result.FromError(fmt.Errorf("resource violates plan: %v", diffs))
}
}
// Save these new outputs to the plan
if resourcePlan, ok := se.deployment.newPlans.get(urn); ok {
resourcePlan.Outputs = outs
} else {
return result.FromError(fmt.Errorf("this should already have a plan from when we called register resources"))
}
// If there is an event subscription for finishing the resource, execute them.
if e := se.opts.Events; e != nil {
if eventerr := e.OnResourceOutputs(reg); eventerr != nil {
@ -176,10 +197,11 @@ func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputs
diagMsg := diag.RawMessage(reg.URN(), outErr.Error())
se.deployment.Diag().Errorf(diagMsg)
se.cancelDueToError()
return
return nil
}
}
e.Done()
return nil
}
// Errored returns whether or not this step executor saw a step whose execution ended in failure.
@ -295,6 +317,11 @@ func (se *stepExecutor) executeStep(workerID int, step Step) error {
if _, hasGoal := se.deployment.goals.get(newState.URN); hasGoal {
se.deployment.news.set(newState.URN, newState)
}
// Update the resource's outputs in the generated plan.
if resourcePlan, ok := se.deployment.newPlans.get(newState.URN); ok {
resourcePlan.Outputs = newState.Outputs
}
}
if events != nil {

View file

@ -179,6 +179,39 @@ func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, res
contract.Assert(len(steps) == 0)
return nil, res
}
// Check each proposed step against the relevant resource plan, if any
for _, s := range steps {
if sg.deployment.plan != nil {
if resourcePlan, ok := sg.deployment.plan.ResourcePlans[s.URN()]; ok {
if len(resourcePlan.Ops) == 0 {
return nil, result.Errorf("%v is not allowed by the plan: no more steps were expected for this resource", s.Op())
}
constraint := resourcePlan.Ops[0]
if !s.Op().ConstrainedTo(constraint) {
return nil, result.Errorf("%v is not allowed by the plan: this resource is constrained to %v", s.Op(), constraint)
}
resourcePlan.Ops = resourcePlan.Ops[1:]
} else {
if !s.Op().ConstrainedTo(OpSame) {
return nil, result.Errorf("%v is not allowed by the plan: no steps were expected for this resource", s.Op())
}
}
}
// Resource plan might be aliased
urn, isAliased := sg.aliased[s.URN()]
if !isAliased {
urn = s.URN()
}
resourcePlan, ok := sg.deployment.newPlans.get(urn)
if !ok {
return nil, result.Errorf("Expected a new resource plan for %v", urn)
}
resourcePlan.Ops = append(resourcePlan.Ops, s.Op())
}
if !sg.isTargetedUpdate() {
return steps, nil
}
@ -272,6 +305,29 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
inputs = processedInputs
}
// Generate the output goal plan
// TODO(pdg-plan): using the program inputs means that non-determinism could sneak in as part of default
// application. However, it is necessary in the face of computed inputs.
newResourcePlan := &ResourcePlan{Goal: NewGoalPlan(oldOutputs, goal)}
sg.deployment.newPlans.set(urn, newResourcePlan)
// If there is a plan for this resource, validate that the program goal conforms to the plan.
// If theres no plan for this resource check that nothing has been changed.
if sg.deployment.plan != nil {
resourcePlan, ok := sg.deployment.plan.ResourcePlans[urn]
if !ok {
if old == nil {
// We could error here, but we'll trigger an error later on anyway that Create isn't valid here
} else if err := checkMissingPlan(old, inputs, goal); err != nil {
return nil, result.FromError(fmt.Errorf("resource violates plan: %w", err))
}
} else {
if err := resourcePlan.checkGoal(oldOutputs, inputs, goal); err != nil {
return nil, result.FromError(fmt.Errorf("resource violates plan: %w", err))
}
}
}
// Produce a new state object that we'll build up as operations are performed. Ultimately, this is what will
// get serialized into the checkpoint file.
new := resource.NewState(goal.Type, urn, goal.Custom, false, "", inputs, nil, goal.Parent, goal.Protect, false,
@ -646,6 +702,13 @@ func (sg *stepGenerator) generateStepsFromDiff(
continue
}
if _, ok := sg.deployment.newPlans.get(dependentResource.URN); !ok {
// We haven't see this resource before, create a new
// resource plan for it with no goal (because it's going to be a delete)
resourcePlan := &ResourcePlan{}
sg.deployment.newPlans.set(dependentResource.URN, resourcePlan)
}
sg.dependentReplaceKeys[dependentResource.URN] = toReplace[i].keys
logging.V(7).Infof("Planner decided to delete '%v' due to dependence on condemned resource '%v'",
@ -751,6 +814,40 @@ func (sg *stepGenerator) GenerateDeletes(targetsOpt map[resource.URN]bool) ([]St
}
}
// Check each proposed delete against the relevant resource plan
for _, s := range dels {
if sg.deployment.plan != nil {
if resourcePlan, ok := sg.deployment.plan.ResourcePlans[s.URN()]; ok {
if len(resourcePlan.Ops) == 0 {
return nil, result.Errorf("%v is not allowed by the plan: no more steps were expected for this resource", s.Op())
}
constraint := resourcePlan.Ops[0]
// We remove the Op from the list before doing the constraint check.
// This is because we look at Ops at the end to see if any expected operations didn't attempt to happen.
// This op has been attempted, it just might fail its constraint.
resourcePlan.Ops = resourcePlan.Ops[1:]
if !s.Op().ConstrainedTo(constraint) {
return nil, result.Errorf("%v is not allowed by the plan: this resource is constrained to %v", s.Op(), constraint)
}
} else {
if !s.Op().ConstrainedTo(OpSame) {
return nil, result.Errorf("%v is not allowed by the plan: no steps were expected for this resource", s.Op())
}
}
}
resourcePlan, ok := sg.deployment.newPlans.get(s.URN())
if !ok {
// TODO(pdg-plan): using the program inputs means that non-determinism could sneak in as part of default
// application. However, it is necessary in the face of computed inputs.
resourcePlan = &ResourcePlan{}
sg.deployment.newPlans.set(s.URN(), resourcePlan)
}
resourcePlan.Ops = append(resourcePlan.Ops, s.Op())
}
// If -target was provided to either `pulumi update` or `pulumi destroy` then only delete
// resources that were specified.
allowedResourcesToDelete, res := sg.determineAllowedResourcesToDeleteFromTargets(targetsOpt)

View file

@ -23,8 +23,6 @@ import (
"reflect"
"strings"
"github.com/blang/semver"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"github.com/pulumi/pulumi/pkg/v3/secrets"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
@ -32,7 +30,6 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
"github.com/santhosh-tekuri/jsonschema/v5"
)
@ -105,23 +102,7 @@ func SerializeDeployment(snap *deploy.Snapshot, sm secrets.Manager, showSecrets
contract.Require(snap != nil, "snap")
// Capture the version information into a manifest.
manifest := apitype.ManifestV1{
Time: snap.Manifest.Time,
Magic: snap.Manifest.Magic,
Version: snap.Manifest.Version,
}
for _, plug := range snap.Manifest.Plugins {
var version string
if plug.Version != nil {
version = plug.Version.String()
}
manifest.Plugins = append(manifest.Plugins, apitype.PluginInfoV1{
Name: plug.Name,
Path: plug.Path,
Type: plug.Kind,
Version: version,
})
}
manifest := snap.Manifest.Serialize()
// If a specific secrets manager was not provided, use the one in the snapshot, if present.
if sm == nil {
@ -223,25 +204,9 @@ func DeserializeUntypedDeployment(
// DeserializeDeploymentV3 deserializes a typed DeploymentV3 into a `deploy.Snapshot`.
func DeserializeDeploymentV3(deployment apitype.DeploymentV3, secretsProv SecretsProvider) (*deploy.Snapshot, error) {
// Unpack the versions.
manifest := deploy.Manifest{
Time: deployment.Manifest.Time,
Magic: deployment.Manifest.Magic,
Version: deployment.Manifest.Version,
}
for _, plug := range deployment.Manifest.Plugins {
var version *semver.Version
if v := plug.Version; v != "" {
sv, err := semver.ParseTolerant(v)
if err != nil {
return nil, err
}
version = &sv
}
manifest.Plugins = append(manifest.Plugins, workspace.PluginInfo{
Name: plug.Name,
Kind: plug.Type,
Version: version,
})
manifest, err := deploy.DeserializeManifest(deployment.Manifest)
if err != nil {
return nil, err
}
var secretsManager secrets.Manager
@ -295,7 +260,7 @@ func DeserializeDeploymentV3(deployment apitype.DeploymentV3, secretsProv Secret
ops = append(ops, desop)
}
return deploy.NewSnapshot(manifest, secretsManager, resources, ops), nil
return deploy.NewSnapshot(*manifest, secretsManager, resources, ops), nil
}
// SerializeResource turns a resource into a structure suitable for serialization.

162
pkg/resource/stack/plan.go Normal file
View 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
}

View file

@ -62,6 +62,7 @@ require (
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/src-d/gcfg v1.4.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect

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

View file

@ -646,3 +646,71 @@ const OutputValueSig = "d0e6a833031e9bbcd3f4e8bde6ca49a4"
func IsInternalPropertyKey(key PropertyKey) bool {
return strings.HasPrefix(string(key), "__")
}
// DeepCopy creates a new copy of this property map.
func (m PropertyMap) DeepCopy() PropertyMap {
copy := PropertyMap{}
for k, v := range m {
copy[k] = v.DeepCopy()
}
return copy
}
// DeepCopy creates a new copy of this property.
func (v PropertyValue) DeepCopy() PropertyValue {
switch {
case v.IsArray():
copy := make([]PropertyValue, len(v.ArrayValue()))
for i, v := range v.ArrayValue() {
copy[i] = v.DeepCopy()
}
return NewArrayProperty(copy)
case v.IsObject():
return NewObjectProperty(v.ObjectValue().DeepCopy())
case v.IsSecret():
return MakeSecret(v.SecretValue().Element.DeepCopy())
default:
return v
}
}
// FindUnknowns returns the set of paths to unknown values nested inside this property map.
func (m PropertyMap) FindUnknowns() []PropertyPath {
var paths []PropertyPath
for k, v := range m {
if v.IsComputed() || v.IsOutput() {
paths = append(paths, PropertyPath{string(k)})
} else {
for _, p := range v.FindUnknowns() {
p = append(PropertyPath{string(k)}, p...)
paths = append(paths, p)
}
}
}
return paths
}
// FindUnknowns returns the set of paths to unknown values nested inside this property map.
func (v PropertyValue) FindUnknowns() []PropertyPath {
switch {
case v.IsArray():
var paths []PropertyPath
for i, v := range v.ArrayValue() {
if v.IsComputed() || v.IsOutput() {
paths = append(paths, PropertyPath{i})
} else {
for _, p := range v.FindUnknowns() {
p = append(PropertyPath{i}, p...)
paths = append(paths, p)
}
}
}
return paths
case v.IsObject():
return v.ObjectValue().FindUnknowns()
case v.IsSecret():
return v.SecretValue().Element.FindUnknowns()
default:
return nil
}
}

View file

@ -78,6 +78,7 @@ func (diff *ObjectDiff) Keys() []PropertyKey {
type ValueDiff struct {
Old PropertyValue // the old value.
New PropertyValue // the new value.
Secret *ValueDiff // the secret value diff (only for secrets)
Array *ArrayDiff // the array's detailed diffs (only for arrays).
Object *ObjectDiff // the object's detailed diffs (only for objects).
}
@ -119,8 +120,7 @@ func (diff *ArrayDiff) Len() int {
// IgnoreKeyFunc is the callback type for Diff's ignore option.
type IgnoreKeyFunc func(key PropertyKey) bool
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
func (props PropertyMap) diff(other PropertyMap, ignoreUnknowns bool, ignoreKeys []IgnoreKeyFunc) *ObjectDiff {
adds := make(PropertyMap)
deletes := make(PropertyMap)
sames := make(PropertyMap)
@ -137,7 +137,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
// First find any updates or deletes.
for k, old := range props {
if ignore(k) {
if ignore(k) || ignoreUnknowns && (old.IsComputed() || old.IsOutput()) {
continue
}
@ -145,7 +145,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
// If a new exists, use it; for output properties, however, ignore differences.
if new.IsOutput() {
sames[k] = old
} else if diff := old.Diff(new, ignoreKeys...); diff != nil {
} else if diff := old.diff(new, ignoreUnknowns, ignoreKeys); diff != nil {
if !old.HasValue() {
adds[k] = new
} else if !new.HasValue() {
@ -156,7 +156,7 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
} else {
sames[k] = old
}
} else if old.HasValue() {
} else if old.HasValue() && (!ignoreUnknowns || !old.IsComputed()) {
// If there was no new property, it has been deleted.
deletes[k] = old
}
@ -185,8 +185,12 @@ func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *O
}
}
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
return props.diff(other, false, ignoreKeys)
}
func (v PropertyValue) diff(other PropertyValue, ignoreUnknowns bool, ignoreKeys []IgnoreKeyFunc) *ValueDiff {
if v.IsArray() && other.IsArray() {
old := v.ArrayValue()
new := other.ArrayValue()
@ -204,7 +208,7 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
sames := make(map[int]PropertyValue)
updates := make(map[int]ValueDiff)
for i := 0; i < len(old) && i < len(new); i++ {
if diff := old[i].Diff(new[i]); diff != nil {
if diff := old[i].diff(new[i], ignoreUnknowns, ignoreKeys); diff != nil {
updates[i] = *diff
} else {
sames[i] = old[i]
@ -228,7 +232,7 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
if v.IsObject() && other.IsObject() {
old := v.ObjectValue()
new := other.ObjectValue()
if diff := old.Diff(new, ignoreKeys...); diff != nil {
if diff := old.diff(new, ignoreUnknowns, ignoreKeys); diff != nil {
return &ValueDiff{
Old: v,
New: other,
@ -237,21 +241,51 @@ func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *V
}
return nil
}
if v.IsSecret() && other.IsSecret() {
old, new := v.SecretValue().Element, other.SecretValue().Element
diff := old.diff(new, ignoreUnknowns, ignoreKeys)
if diff != nil {
return &ValueDiff{
Old: v,
New: other,
Secret: diff,
}
}
return nil
}
if v.IsResourceReference() && other.IsResourceReference() {
vr := v.ResourceReferenceValue()
or := other.ResourceReferenceValue()
if vr.URN != or.URN {
return &ValueDiff{Old: v, New: other}
}
vid, oid := vr.ID, or.ID
if vid.IsComputed() && ignoreUnknowns || vid.DeepEquals(oid) {
return nil
}
return &ValueDiff{Old: v, New: other}
}
// If we got here, either the values are primitives, or they weren't the same type; do a simple diff.
if v.DeepEquals(other) {
if v.DeepEquals(other) || ignoreUnknowns && (v.IsComputed() || v.IsOutput()) {
return nil
}
return &ValueDiff{Old: v, New: other}
}
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
func (props PropertyMap) DeepEquals(other PropertyMap) bool {
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
return v.diff(other, false, ignoreKeys)
}
func (props PropertyMap) deepEquals(other PropertyMap, includeUnknowns bool) bool {
// If any in props either doesn't exist, or is of a different value, return false.
for _, k := range props.StableKeys() {
v := props[k]
if p, has := other[k]; has {
if !v.DeepEquals(p) {
if !v.deepEquals(p, includeUnknowns) {
return false
}
} else if v.HasValue() {
@ -269,8 +303,19 @@ func (props PropertyMap) DeepEquals(other PropertyMap) bool {
return true
}
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
func (v PropertyValue) DeepEquals(other PropertyValue) bool {
func (v PropertyValue) deepEquals(other PropertyValue, includeUnknowns bool) bool {
// Computed values are always equal.
if v.IsComputed() && other.IsComputed() {
return true
}
// If includeUnknowns is true then anything is equal to a computed
if includeUnknowns {
if v.IsComputed() || other.IsComputed() {
return true
}
}
// Arrays are equal if they are both of the same size and elements are deeply equal.
if v.IsArray() {
if !other.IsArray() {
@ -282,7 +327,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
return false
}
for i, elem := range va {
if !elem.DeepEquals(oa[i]) {
if !elem.deepEquals(oa[i], includeUnknowns) {
return false
}
}
@ -309,7 +354,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
}
vo := v.ObjectValue()
oa := other.ObjectValue()
return vo.DeepEquals(oa)
return vo.deepEquals(oa, includeUnknowns)
}
// Secret are equal if the value they wrap are equal.
@ -320,7 +365,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
vs := v.SecretValue()
os := other.SecretValue()
return vs.Element.DeepEquals(os.Element)
return vs.Element.deepEquals(os.Element, includeUnknowns)
}
// Resource references are equal if they refer to the same resource. The package version is ignored.
@ -339,7 +384,7 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
if vid.IsComputed() && oid.IsComputed() {
return true
}
return vid.DeepEquals(oid)
return vid.deepEquals(oid, includeUnknowns)
}
// Outputs are equal if each of their fields is deeply equal.
@ -367,9 +412,28 @@ func (v PropertyValue) DeepEquals(other PropertyValue) bool {
}
}
return vo.Element.DeepEquals(oo.Element)
return vo.Element.deepEquals(oo.Element, includeUnknowns)
}
// For all other cases, primitives are equal if their values are equal.
return v.V == other.V
}
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
func (props PropertyMap) DeepEquals(other PropertyMap) bool {
return props.deepEquals(other, false)
}
// DeepEquals returns true if this property value is deeply equal to the other property value; and false otherwise.
func (v PropertyValue) DeepEquals(other PropertyValue) bool {
return v.deepEquals(other, false)
}
func (props PropertyMap) DiffIncludeUnknowns(other PropertyMap) (*ObjectDiff, bool) {
diff := props.diff(other, true, nil)
return diff, diff != nil
}
func (v PropertyValue) DeepEqualsIncludeUnknowns(other PropertyValue) bool {
return v.deepEquals(other, true)
}

View file

@ -1,6 +1,8 @@
package resource
import (
"bytes"
"fmt"
"strconv"
"strings"
@ -291,3 +293,39 @@ func (p PropertyPath) Contains(other PropertyPath) bool {
return true
}
func requiresQuote(c rune) bool {
return !(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_')
}
func (p PropertyPath) String() string {
var buf bytes.Buffer
for i, k := range p {
switch k := k.(type) {
case string:
var keyBuf bytes.Buffer
quoted := false
for _, c := range k {
if requiresQuote(c) {
quoted = true
if c == '"' {
keyBuf.WriteByte('\\')
}
}
keyBuf.WriteRune(c)
}
if !quoted {
if i == 0 {
fmt.Fprintf(&buf, "%s", keyBuf.String())
} else {
fmt.Fprintf(&buf, ".%s", keyBuf.String())
}
} else {
fmt.Fprintf(&buf, `["%s"]`, keyBuf.String())
}
case int:
fmt.Fprintf(&buf, "[%d]", k)
}
}
return buf.String()
}

View file

@ -49,68 +49,84 @@ func TestPropertyPath(t *testing.T) {
}))
cases := []struct {
path string
parsed PropertyPath
path string
parsed PropertyPath
expected string
}{
{
"root",
PropertyPath{"root"},
"root",
},
{
"root.nested",
PropertyPath{"root", "nested"},
"root.nested",
},
{
`root["nested"]`,
PropertyPath{"root", "nested"},
`root.nested`,
},
{
"root.double.nest",
PropertyPath{"root", "double", "nest"},
"root.double.nest",
},
{
`root["double"].nest`,
PropertyPath{"root", "double", "nest"},
`root.double.nest`,
},
{
`root["double"]["nest"]`,
PropertyPath{"root", "double", "nest"},
`root.double.nest`,
},
{
"root.array[0]",
PropertyPath{"root", "array", 0},
"root.array[0]",
},
{
"root.array[1]",
PropertyPath{"root", "array", 1},
"root.array[1]",
},
{
"root.array[0].nested",
PropertyPath{"root", "array", 0, "nested"},
"root.array[0].nested",
},
{
"root.array2[0][1].nested",
PropertyPath{"root", "array2", 0, 1, "nested"},
"root.array2[0][1].nested",
},
{
"root.nested.array[0].double[1]",
PropertyPath{"root", "nested", "array", 0, "double", 1},
"root.nested.array[0].double[1]",
},
{
`root["key with \"escaped\" quotes"]`,
PropertyPath{"root", `key with "escaped" quotes`},
`root["key with \"escaped\" quotes"]`,
},
{
`root["key with a ."]`,
PropertyPath{"root", "key with a ."},
`root["key with a ."]`,
},
{
`["root key with \"escaped\" quotes"].nested`,
PropertyPath{`root key with "escaped" quotes`, "nested"},
`["root key with \"escaped\" quotes"].nested`,
},
{
`["root key with a ."][1]`,
PropertyPath{"root key with a .", 1},
`["root key with a ."][1]`,
},
}
@ -119,6 +135,7 @@ func TestPropertyPath(t *testing.T) {
parsed, err := ParsePropertyPath(c.path)
assert.NoError(t, err)
assert.Equal(t, c.parsed, parsed)
assert.Equal(t, c.expected, parsed.String())
v, ok := parsed.Get(value)
assert.True(t, ok)