// Copyright 2016-2023, 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 ( "context" "errors" "testing" "github.com/pulumi/pulumi/pkg/v3/display" "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" "github.com/pulumi/pulumi/pkg/v3/util/gsync" "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/urn" "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "github.com/stretchr/testify/assert" ) func TestRawPrefix(t *testing.T) { t.Parallel() tests := []struct { name string op display.StepOp want string }{ {name: "Same", op: OpSame, want: " "}, {name: "Create", op: OpCreate, want: "+ "}, {name: "Delete", op: OpDelete, want: "- "}, {name: "Update", op: OpUpdate, want: "~ "}, {name: "Replace", op: OpReplace, want: "+-"}, {name: "CreateReplacement", op: OpCreateReplacement, want: "++"}, {name: "DeleteReplaced", op: OpDeleteReplaced, want: "--"}, {name: "Read", op: OpRead, want: "> "}, {name: "ReadReplacement", op: OpReadReplacement, want: ">>"}, {name: "Refresh", op: OpRefresh, want: "~ "}, {name: "ReadDiscard", op: OpReadDiscard, want: "< "}, {name: "DiscardReplaced", op: OpDiscardReplaced, want: "<<"}, {name: "Import", op: OpImport, want: "= "}, {name: "ImportReplacement", op: OpImportReplacement, want: "=>"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() assert.Equal(t, tt.want, RawPrefix(tt.op)) }) } t.Run("panics on unknown", func(t *testing.T) { t.Parallel() assert.Panics(t, func() { RawPrefix("not-a-real-operation") }) }) } func TestPastTense(t *testing.T) { t.Parallel() tests := []struct { name string op display.StepOp want string }{ {"Same", OpSame, "samed"}, {"Create", OpCreate, "created"}, {"Replace", OpReplace, "replaced"}, {"Update", OpUpdate, "updated"}, // TODO(dixler) consider fixing this. {"CreateReplacement", OpCreateReplacement, "create-replacementd"}, {"ReadReplacement", OpReadReplacement, "read-replacementd"}, {"Refresh", OpRefresh, "refreshed"}, {"Read", OpRead, "read"}, {"ReadDiscard", OpReadDiscard, "discarded"}, {"DiscardReplaced", OpDiscardReplaced, "discarded"}, {"Delete", OpDelete, "deleted"}, {"DeleteReplaced", OpDeleteReplaced, "deleted"}, {"Import", OpImport, "imported"}, {"ImportReplacement", OpImportReplacement, "imported"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() assert.Equal(t, tt.want, PastTense(tt.op)) }) } t.Run("panics on unknown", func(t *testing.T) { t.Parallel() assert.Panics(t, func() { PastTense("not-a-real-operation") }) }) } func TestSameStep(t *testing.T) { t.Parallel() t.Run("Apply", func(t *testing.T) { t.Parallel() t.Run("bad provider state for resource", func(t *testing.T) { t.Parallel() s := &SameStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, old: &resource.State{ URN: "urn:pulumi:stack::project::type::foo", }, new: &resource.State{ URN: "urn:pulumi:stack::project::type::foo", Type: "pulumi:providers:some-provider", }, } _, _, err := s.Apply() assert.ErrorContains(t, err, "bad provider state for resource") }) }) } func TestCreateStep(t *testing.T) { t.Parallel() t.Run("Apply", func(t *testing.T) { t.Parallel() t.Run("custom", func(t *testing.T) { t.Parallel() t.Run("error getting provider", func(t *testing.T) { t.Parallel() s := &CreateStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, new: &resource.State{ Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "Default provider for 'default_5_42_0' disabled.") assert.Equal(t, resource.StatusOK, status) }) t.Run("error in create", func(t *testing.T) { t.Parallel() expectedErr := errors.New("expected error") var createCalled bool s := &CreateStep{ new: &resource.State{ Custom: true, }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ CreateF: func(context.Context, plugin.CreateRequest) (plugin.CreateResponse, error) { createCalled = true return plugin.CreateResponse{}, expectedErr }, }, } status, _, err := s.Apply() assert.ErrorIs(t, err, expectedErr) assert.True(t, createCalled) assert.Equal(t, resource.StatusOK, status) }) t.Run("handle InitError", func(t *testing.T) { t.Parallel() s := &CreateStep{ new: &resource.State{ Custom: true, }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ CreateF: func(context.Context, plugin.CreateRequest) (plugin.CreateResponse, error) { return plugin.CreateResponse{ Status: resource.StatusPartialFailure, }, &plugin.InitError{ Reasons: []string{ "intentional error", }, } }, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "intentional error") assert.Len(t, s.new.InitErrors, 1) assert.Equal(t, resource.StatusPartialFailure, status) }) t.Run("error create no ID", func(t *testing.T) { t.Parallel() s := &CreateStep{ new: &resource.State{ Custom: true, }, deployment: &Deployment{ opts: &Options{}, }, provider: &deploytest.Provider{ CreateF: func(context.Context, plugin.CreateRequest) (plugin.CreateResponse, error) { return plugin.CreateResponse{}, nil }, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "provider did not return an ID from Create") assert.Equal(t, resource.StatusOK, status) }) }) }) } func TestDeleteStep(t *testing.T) { t.Parallel() t.Run("isDeletedWith", func(t *testing.T) { t.Parallel() otherDeletions := map[resource.URN]bool{ "false-key": false, "true-key": true, } assert.False(t, isDeletedWith("", otherDeletions)) assert.False(t, isDeletedWith("does-not-exist", otherDeletions)) assert.False(t, isDeletedWith("false-key", otherDeletions)) assert.True(t, isDeletedWith("true-key", otherDeletions)) }) t.Run("Apply", func(t *testing.T) { t.Parallel() t.Run("custom", func(t *testing.T) { t.Parallel() t.Run("error getting provider", func(t *testing.T) { t.Parallel() s := &DeleteStep{ deployment: &Deployment{ opts: &Options{ DryRun: false, }, }, old: &resource.State{ Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "Default provider for 'default_5_42_0' disabled.") assert.Equal(t, resource.StatusOK, status) }) }) }) } func TestRemovePendingReplaceStep(t *testing.T) { t.Parallel() t.Run("NewRemovePendingReplaceStep", func(t *testing.T) { t.Parallel() t.Run("panics on old=nil", func(t *testing.T) { t.Parallel() assert.Panics(t, func() { NewRemovePendingReplaceStep(nil, nil) }) }) t.Run("panics if not old.PendingReplacement", func(t *testing.T) { t.Parallel() assert.Panics(t, func() { NewRemovePendingReplaceStep(nil, &resource.State{ PendingReplacement: false, }) }) }) }) t.Run("Op", func(t *testing.T) { t.Parallel() s := NewRemovePendingReplaceStep(nil, &resource.State{ PendingReplacement: true, }) assert.Equal(t, OpRemovePendingReplace, s.Op()) }) t.Run("Deployment", func(t *testing.T) { t.Parallel() d := &Deployment{} s := NewRemovePendingReplaceStep(d, &resource.State{ Type: "expected-value", PendingReplacement: true, }) assert.Equal(t, d, s.Deployment()) }) t.Run("Type", func(t *testing.T) { t.Parallel() s := NewRemovePendingReplaceStep(nil, &resource.State{ Type: "expected-value", PendingReplacement: true, }) assert.Equal(t, tokens.Type("expected-value"), s.Type()) }) t.Run("Provider", func(t *testing.T) { t.Parallel() s := NewRemovePendingReplaceStep(nil, &resource.State{ Provider: "expected-value", PendingReplacement: true, }) assert.Equal(t, "expected-value", s.Provider()) }) t.Run("URN", func(t *testing.T) { t.Parallel() s := NewRemovePendingReplaceStep(nil, &resource.State{ URN: "expected-value", PendingReplacement: true, }) assert.Equal(t, resource.URN("expected-value"), s.URN()) }) t.Run("Old", func(t *testing.T) { t.Parallel() old := &resource.State{ URN: "expected-value", PendingReplacement: true, } s := NewRemovePendingReplaceStep(nil, old) assert.Equal(t, old, s.Old()) }) t.Run("New", func(t *testing.T) { t.Parallel() s := NewRemovePendingReplaceStep(nil, &resource.State{ PendingReplacement: true, }) assert.Equal(t, (*resource.State)(nil), s.New()) }) t.Run("Res", func(t *testing.T) { t.Parallel() old := &resource.State{ PendingReplacement: true, } s := NewRemovePendingReplaceStep(nil, old) assert.Equal(t, old, s.Res()) }) t.Run("Logical", func(t *testing.T) { t.Parallel() s := NewRemovePendingReplaceStep(nil, &resource.State{ PendingReplacement: true, }) assert.False(t, s.Logical()) }) t.Run("Apply", func(t *testing.T) { t.Parallel() d := &Deployment{ opts: &Options{ DryRun: true, }, } s := NewRemovePendingReplaceStep(d, &resource.State{ PendingReplacement: true, }) status, _, err := s.Apply() assert.NoError(t, err) assert.Equal(t, resource.StatusOK, status) }) } func TestUpdateStep(t *testing.T) { t.Parallel() t.Run("Apply", func(t *testing.T) { t.Parallel() t.Run("error getting provider", func(t *testing.T) { t.Parallel() s := &UpdateStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, old: &resource.State{}, new: &resource.State{ Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "Default provider for 'default_5_42_0' disabled.") assert.Equal(t, resource.StatusOK, status) }) t.Run("failure in provider", func(t *testing.T) { t.Parallel() expectedErr := errors.New("expected error") s := &UpdateStep{ old: &resource.State{}, new: &resource.State{ Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ UpdateF: func(context.Context, plugin.UpdateRequest) (plugin.UpdateResponse, error) { return plugin.UpdateResponse{}, expectedErr }, }, } status, _, err := s.Apply() assert.ErrorIs(t, err, expectedErr) assert.Equal(t, resource.StatusOK, status) }) t.Run("partial failure in provider", func(t *testing.T) { t.Parallel() s := &UpdateStep{ old: &resource.State{}, new: &resource.State{ Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ UpdateF: func(context.Context, plugin.UpdateRequest) (plugin.UpdateResponse, error) { return plugin.UpdateResponse{ Properties: resource.PropertyMap{ "key": resource.NewStringProperty("expected-value"), }, Status: resource.StatusPartialFailure, }, &plugin.InitError{ Reasons: []string{ "intentional error", }, } }, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "intentional error") assert.Equal(t, resource.StatusPartialFailure, status) // News should be updated. assert.Len(t, s.new.InitErrors, 1) assert.Equal(t, resource.PropertyMap{ "key": resource.NewStringProperty("expected-value"), }, s.new.Outputs) }) }) } func TestReplaceStep(t *testing.T) { t.Parallel() t.Run("Deployment", func(t *testing.T) { t.Parallel() d := &Deployment{} s := ReplaceStep{deployment: d} assert.Equal(t, d, s.Deployment()) }) } func TestReadStep(t *testing.T) { t.Parallel() t.Run("Apply", func(t *testing.T) { t.Parallel() t.Run("error getting provider", func(t *testing.T) { t.Parallel() s := &ReadStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, old: &resource.State{}, new: &resource.State{ Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "Default provider for 'default_5_42_0' disabled.") assert.Equal(t, resource.StatusOK, status) }) t.Run("failure in provider", func(t *testing.T) { t.Parallel() expectedErr := errors.New("expected error") s := &ReadStep{ old: &resource.State{}, new: &resource.State{ URN: "some-urn", ID: "some-id", Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{}, expectedErr }, }, } status, _, err := s.Apply() assert.ErrorIs(t, err, expectedErr) assert.Equal(t, resource.StatusOK, status) }) t.Run("partial failure in provider", func(t *testing.T) { t.Parallel() s := &ReadStep{ old: &resource.State{}, new: &resource.State{ URN: "some-urn", ID: "some-id", Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{ ReadResult: plugin.ReadResult{ ID: "new-id", Inputs: resource.PropertyMap{ "inputs-key": resource.NewStringProperty("expected-value"), }, Outputs: resource.PropertyMap{ "outputs-key": resource.NewStringProperty("expected-value"), }, }, Status: resource.StatusPartialFailure, }, &plugin.InitError{ Reasons: []string{ "intentional error", }, } }, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "intentional error") assert.Equal(t, resource.StatusPartialFailure, status) // News should be updated. assert.Len(t, s.new.InitErrors, 1) assert.Equal(t, (resource.PropertyMap)(nil), s.new.Inputs) assert.Equal(t, resource.PropertyMap{ "outputs-key": resource.NewStringProperty("expected-value"), }, s.new.Outputs) assert.Equal(t, resource.ID("new-id"), s.new.ID) }) t.Run("unknown id", func(t *testing.T) { t.Parallel() s := &ReadStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, new: &resource.State{ ID: plugin.UnknownStringValue, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { panic("should not be called") }, }, } status, _, err := s.Apply() assert.NoError(t, err) assert.Equal(t, resource.StatusOK, status) // News should be updated. assert.Equal(t, resource.PropertyMap{}, s.new.Outputs) }) }) } func TestRefreshStepPatterns(t *testing.T) { t.Parallel() tests := []struct { name string inputs resource.PropertyMap outputs resource.PropertyMap readInputs resource.PropertyMap readOutputs resource.PropertyMap diffResult plugin.DiffResult expectedDetailedDiff map[string]plugin.PropertyDiff ignoreChanges []string }{ { name: "tfbridge 'computed' property changed", inputs: resource.PropertyMap{}, outputs: resource.PropertyMap{ "etag": resource.NewStringProperty("abc"), }, readInputs: resource.PropertyMap{}, readOutputs: resource.PropertyMap{ "etag": resource.NewStringProperty("def"), }, diffResult: plugin.DiffResult{ // Diff newInputs, newOutputs, oldInputs Changes: plugin.DiffNone, DetailedDiff: map[string]plugin.PropertyDiff{}, }, expectedDetailedDiff: map[string]plugin.PropertyDiff{}, }, { // Note: this is probably a case where the TF provider has a bug, a pure in property // really shouldn't change, but this is common in TF providers. name: "tfbridge 'required' property changed", inputs: resource.PropertyMap{ "title": resource.NewStringProperty("test"), }, outputs: resource.PropertyMap{ "title": resource.NewStringProperty("test"), }, readInputs: resource.PropertyMap{ "title": resource.NewStringProperty("testtesttest"), }, readOutputs: resource.PropertyMap{ "title": resource.NewStringProperty("testtesttest"), }, diffResult: plugin.DiffResult{ // Diff newInputs, newOutputs, oldInputs Changes: plugin.DiffSome, DetailedDiff: map[string]plugin.PropertyDiff{ "title": {Kind: plugin.DiffUpdate}, }, }, expectedDetailedDiff: map[string]plugin.PropertyDiff{ "title": {Kind: plugin.DiffUpdate}, }, }, { // Note: this is probably a case where the TF provider has a bug, a pure in property // really shouldn't change, but this is common in TF providers. name: "tfbridge 'required' property changed w/ ignoreChanges", ignoreChanges: []string{"title"}, inputs: resource.PropertyMap{ "title": resource.NewStringProperty("test"), }, outputs: resource.PropertyMap{ "title": resource.NewStringProperty("test"), }, readInputs: resource.PropertyMap{ "title": resource.NewStringProperty("testtesttest"), }, readOutputs: resource.PropertyMap{ "title": resource.NewStringProperty("testtesttest"), }, diffResult: plugin.DiffResult{ // Diff newInputs, newOutputs, oldInputs Changes: plugin.DiffNone, DetailedDiff: map[string]plugin.PropertyDiff{}, }, expectedDetailedDiff: map[string]plugin.PropertyDiff{}, }, { // Note: this is probably a case where the TF provider has a bug, a pure in property // really shouldn't change, but this is common in TF providers. name: "tfbridge 'optional' property changed", inputs: resource.PropertyMap{}, outputs: resource.PropertyMap{ "body": resource.NewStringProperty(""), }, readInputs: resource.PropertyMap{ // Pretty sure its a bug in tfbridge that it doesn't populate the new value // into inputs for an `optional` property. But that's what it does so // testing against the current behaviour. }, readOutputs: resource.PropertyMap{ "body": resource.NewStringProperty("bodybodybody"), }, diffResult: plugin.DiffResult{ // Diff newInputs, newOutputs, oldInputs Changes: plugin.DiffSome, DetailedDiff: map[string]plugin.PropertyDiff{ "body": {Kind: plugin.DiffDelete}, }, }, expectedDetailedDiff: map[string]plugin.PropertyDiff{ "body": {Kind: plugin.DiffAdd}, }, }, { // Note: this is probably a case where the TF provider has a bug, a pure in property // really shouldn't change, but this is common in TF providers. name: "tfbridge 'optional' property changed w/ ignoreChanges", ignoreChanges: []string{"body"}, inputs: resource.PropertyMap{}, outputs: resource.PropertyMap{ "body": resource.NewStringProperty(""), }, readInputs: resource.PropertyMap{ // Pretty sure its a bug in tfbridge that it doesn't populate the new value // into inputs for an `optional` property. But that's what it does so // testing against the current behaviour. }, readOutputs: resource.PropertyMap{ "body": resource.NewStringProperty("bodybodybody"), }, diffResult: plugin.DiffResult{ // Diff newInputs, newOutputs, oldInputs Changes: plugin.DiffNone, DetailedDiff: map[string]plugin.PropertyDiff{}, }, expectedDetailedDiff: map[string]plugin.PropertyDiff{}, }, { name: "tfbridge 'optional+computed' property element added", inputs: resource.PropertyMap{}, outputs: resource.PropertyMap{ "tags": resource.NewObjectProperty(resource.PropertyMap{}), }, readInputs: resource.PropertyMap{}, readOutputs: resource.PropertyMap{ "tags": resource.NewObjectProperty((resource.PropertyMap{ "foo": resource.NewStringProperty("bar"), })), }, diffResult: plugin.DiffResult{ // Diff newInputs, newOutputs, oldInputs Changes: plugin.DiffSome, DetailedDiff: map[string]plugin.PropertyDiff{ "tags": {Kind: plugin.DiffUpdate}, "tags.foo": {Kind: plugin.DiffDelete}, }, }, expectedDetailedDiff: map[string]plugin.PropertyDiff{ "tags": {Kind: plugin.DiffUpdate}, "tags.foo": {Kind: plugin.DiffAdd}, }, }, { name: "tfbridge 'optional+computed' property element changed", inputs: resource.PropertyMap{ "tags": resource.NewObjectProperty(resource.PropertyMap{ "foo": resource.NewStringProperty("bar"), }), }, outputs: resource.PropertyMap{ "tags": resource.NewObjectProperty(resource.PropertyMap{ "foo": resource.NewStringProperty("bar"), }), }, readInputs: resource.PropertyMap{ "tags": resource.NewObjectProperty((resource.PropertyMap{ "foo": resource.NewStringProperty("baz"), })), }, readOutputs: resource.PropertyMap{ "tags": resource.NewObjectProperty((resource.PropertyMap{ "foo": resource.NewStringProperty("baz"), })), }, diffResult: plugin.DiffResult{ // Diff newInputs, newOutputs, oldInputs Changes: plugin.DiffSome, DetailedDiff: map[string]plugin.PropertyDiff{ "tags.foo": {Kind: plugin.DiffUpdate}, }, }, expectedDetailedDiff: map[string]plugin.PropertyDiff{ "tags.foo": {Kind: plugin.DiffUpdate}, }, }, } for _, tc := range tests { s := &RefreshStep{ old: &resource.State{ URN: "some-urn", ID: "some-id", Type: "some-type", Custom: true, Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", Inputs: tc.inputs, Outputs: tc.outputs, }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{ ReadResult: plugin.ReadResult{ ID: req.ID, Inputs: tc.readInputs, Outputs: tc.readOutputs, }, Status: resource.StatusOK, }, nil }, DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResponse, error) { return tc.diffResult, nil }, }, } status, _, err := s.Apply() assert.Equal(t, s.diff.DetailedDiff, tc.expectedDetailedDiff) assert.NoError(t, err) assert.Equal(t, resource.StatusOK, status) } } func TestRefreshStep(t *testing.T) { t.Parallel() t.Run("Apply", func(t *testing.T) { t.Parallel() t.Run("error getting provider", func(t *testing.T) { t.Parallel() s := &RefreshStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, old: &resource.State{ Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "Default provider for 'default_5_42_0' disabled.") assert.Equal(t, resource.StatusOK, status) }) t.Run("failure in provider", func(t *testing.T) { t.Parallel() expectedErr := errors.New("expected error") s := &RefreshStep{ old: &resource.State{ URN: "some-urn", ID: "some-id", Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{}, expectedErr }, }, } status, _, err := s.Apply() assert.ErrorIs(t, err, expectedErr) assert.Equal(t, resource.StatusOK, status) }) t.Run("partial failure in provider", func(t *testing.T) { t.Parallel() s := &RefreshStep{ old: &resource.State{ URN: "some-urn", ID: "some-id", Type: "some-type", Custom: true, // Use denydefaultprovider ID to ensure failure. Provider: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0::denydefaultprovider", }, deployment: &Deployment{ ctx: &plugin.Context{Diag: &deploytest.NoopSink{}}, opts: &Options{ DryRun: true, }, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{ ReadResult: plugin.ReadResult{ ID: "new-id", Inputs: resource.PropertyMap{ "inputs-key": resource.NewStringProperty("expected-value"), }, Outputs: resource.PropertyMap{ "outputs-key": resource.NewStringProperty("expected-value"), }, }, Status: resource.StatusPartialFailure, }, &plugin.InitError{ Reasons: []string{ "intentional error", }, } }, }, } status, _, err := s.Apply() assert.NoError(t, err, "InitError should be discarded") assert.Equal(t, resource.StatusPartialFailure, status) // News should be updated. assert.Len(t, s.new.InitErrors, 1) assert.Equal(t, resource.PropertyMap{ "outputs-key": resource.NewStringProperty("expected-value"), }, s.new.Outputs) assert.Equal(t, resource.ID("new-id"), s.new.ID) }) }) } func TestImportStep(t *testing.T) { t.Parallel() t.Run("Apply", func(t *testing.T) { t.Parallel() t.Run("missing parent", func(t *testing.T) { t.Parallel() s := &ImportStep{ planned: true, new: &resource.State{ Parent: "urn:pulumi:stack::project::foo:bar:Bar::name", }, deployment: &Deployment{ opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "unknown parent") assert.Equal(t, resource.StatusOK, status) }) t.Run("getProvider error", func(t *testing.T) { t.Parallel() s := &ImportStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, new: &resource.State{ URN: "urn:pulumi:stack::project::foo:bar:Bar::name", ID: "some-id", Custom: true, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "bad provider reference") assert.Equal(t, resource.StatusOK, status) }) t.Run("provider read error", func(t *testing.T) { t.Parallel() t.Run("error", func(t *testing.T) { t.Parallel() expectedErr := errors.New("expected error") s := &ImportStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, new: &resource.State{ URN: "urn:pulumi:stack::project::foo:bar:Bar::name", ID: "some-id", Custom: true, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{}, expectedErr }, }, } status, _, err := s.Apply() assert.ErrorIs(t, err, expectedErr) assert.Equal(t, resource.StatusOK, status) }) t.Run("init error", func(t *testing.T) { t.Parallel() s := &ImportStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, new: &resource.State{ URN: "urn:pulumi:stack::project::foo:bar:Bar::name", ID: "some-id", Custom: true, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{}, &plugin.InitError{ Reasons: []string{ "intentional error", }, } }, }, } status, _, err := s.Apply() assert.Error(t, err) assert.Equal(t, resource.StatusOK, status) assert.Len(t, s.new.InitErrors, 1) }) t.Run("resource does not exist", func(t *testing.T) { t.Parallel() s := &ImportStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, new: &resource.State{ URN: "urn:pulumi:stack::project::foo:bar:Bar::name", ID: "some-id", Custom: true, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{}, nil }, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "does not exist") assert.Equal(t, resource.StatusOK, status) }) t.Run("provider does not support importing resources", func(t *testing.T) { t.Parallel() s := &ImportStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, new: &resource.State{ URN: "urn:pulumi:stack::project::foo:bar:Bar::name", ID: "some-id", Custom: true, }, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { return plugin.ReadResponse{ ReadResult: plugin.ReadResult{ Outputs: resource.PropertyMap{}, }, Status: resource.StatusOK, }, nil }, }, } status, _, err := s.Apply() assert.ErrorContains(t, err, "provider does not support importing resources") assert.Equal(t, resource.StatusOK, status) }) }) t.Run("provider check error", func(t *testing.T) { t.Parallel() t.Run("error", func(t *testing.T) { t.Parallel() expectedErr := errors.New("expected error") var readCalled bool var checkCalled bool s := &ImportStep{ deployment: &Deployment{ opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, new: &resource.State{ URN: "urn:pulumi:stack::project::foo:bar:Bar::name", ID: "some-id", Type: "foo:bar:Bar", Custom: true, }, randomSeed: []byte{}, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { readCalled = true return plugin.ReadResponse{ ReadResult: plugin.ReadResult{ Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}, }, Status: resource.StatusOK, }, nil }, CheckF: func(context.Context, plugin.CheckRequest) (plugin.CheckResponse, error) { checkCalled = true return plugin.CheckResponse{}, expectedErr }, }, } _, _, err := s.Apply() assert.ErrorIs(t, err, expectedErr) assert.True(t, readCalled) assert.True(t, checkCalled) }) t.Run("failures", func(t *testing.T) { t.Parallel() var readCalled bool var checkCalled bool s := &ImportStep{ deployment: &Deployment{ ctx: &plugin.Context{ Diag: &deploytest.NoopSink{}, }, opts: &Options{ DryRun: true, }, olds: map[resource.URN]*resource.State{}, news: &gsync.Map[urn.URN, *resource.State]{}, }, new: &resource.State{ URN: "urn:pulumi:stack::project::foo:bar:Bar::name", ID: "some-id", Type: "foo:bar:Bar", Custom: true, Parent: "urn:pulumi:stack::project::pulumi:pulumi:Stack::name", Provider: "urn:pulumi:stack::project::pulumi:providers:provider::name::uuid", }, planned: true, randomSeed: []byte{}, provider: &deploytest.Provider{ ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) { readCalled = true return plugin.ReadResponse{ ReadResult: plugin.ReadResult{ Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}, }, Status: resource.StatusOK, }, nil }, CheckF: func(context.Context, plugin.CheckRequest) (plugin.CheckResponse, error) { checkCalled = true return plugin.CheckResponse{ Properties: resource.PropertyMap{}, Failures: []plugin.CheckFailure{ { Reason: "intentional failure", }, }, }, nil }, }, } _, _, err := s.Apply() assert.NoError(t, err) assert.True(t, readCalled) assert.True(t, checkCalled) }) }) }) } func TestGetProvider(t *testing.T) { t.Parallel() t.Run("ensure default is not selected", func(t *testing.T) { t.Parallel() s := &CreateStep{ new: &resource.State{ Provider: "invalid-provider", }, } prov, err := getProvider(s, s.provider) assert.Nil(t, prov) assert.ErrorContains(t, err, "bad provider reference") }) t.Run("ensure default is not selected", func(t *testing.T) { t.Parallel() expectedProvider := &deploytest.Provider{} s := &CreateStep{ provider: expectedProvider, new: &resource.State{ Provider: "invalid-provider", }, } prov, err := getProvider(s, s.provider) assert.NoError(t, err) assert.Equal(t, expectedProvider, prov) }) } func TestSuffix(t *testing.T) { t.Parallel() for op, expectation := range map[display.StepOp]string{ OpSame: "", OpCreate: "", OpDelete: "", OpDeleteReplaced: "", OpRead: "", OpReadDiscard: "", OpDiscardReplaced: "", OpRemovePendingReplace: "", OpImport: "", "not-a-real-step-op": "", OpCreateReplacement: colors.Reset, OpUpdate: colors.Reset, OpReplace: colors.Reset, OpReadReplacement: colors.Reset, OpRefresh: colors.Reset, OpImportReplacement: colors.Reset, } { assert.Equal(t, expectation, Suffix(op)) } }