pulumi/pkg/resource/deploy/step_test.go

1420 lines
42 KiB
Go

// 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 (
"bytes"
"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/testing/diagtest"
"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)
})
})
t.Run("preview: resource input diff found -> no error, 4 diag msg", func(t *testing.T) {
t.Parallel()
var diffCalled bool
var stderrbuff, stdoutbuff bytes.Buffer
ctx, _ := plugin.NewContext(
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for messages.
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for status messages.
nil, // the host that can be used to fetch providers.
nil, // configSource
t.TempDir(), // the working directory to spawn all plugins in.
nil, // runtimeOptions
false, // disableProviderPreview
nil,
)
s := &ImportStep{
deployment: &Deployment{
opts: &Options{
DryRun: true, // DryRun true denotes preview
},
olds: map[resource.URN]*resource.State{},
news: &gsync.Map[urn.URN, *resource.State]{},
ctx: ctx,
},
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{
DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResponse, error) {
diffCalled = true
return plugin.DiffResponse{
Changes: plugin.DiffSome,
ChangedKeys: []resource.PropertyKey{
resource.PropertyKey("non-matching-input-1"),
resource.PropertyKey("non-matching-input-2"),
},
}, nil
},
},
}
_, _, err := s.Apply()
assert.NoError(t, err)
assert.True(t, diffCalled)
assert.Contains(t, stderrbuff.String(), "[non-matching-input-1 non-matching-input-2]")
})
t.Run("preview: no resource input diff found -> no error, no msg", func(t *testing.T) {
t.Parallel()
var stderrbuff, stdoutbuff bytes.Buffer
ctx, _ := plugin.NewContext(
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for messages.
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for status messages.
nil, // the host that can be used to fetch providers.
nil, // configSource
t.TempDir(), // the working directory to spawn all plugins in.
nil, // runtimeOptions
false, // disableProviderPreview
nil,
)
s := &ImportStep{
deployment: &Deployment{
opts: &Options{
DryRun: true, // DryRun true denotes preview
},
olds: map[resource.URN]*resource.State{},
news: &gsync.Map[urn.URN, *resource.State]{},
ctx: ctx,
},
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{},
}
_, _, err := s.Apply()
assert.NoError(t, err)
assert.Equal(t, 0, len(stderrbuff.String()))
})
t.Run("up: resource input diff found -> error, no msg", func(t *testing.T) {
t.Parallel()
var diffCalled bool
var stderrbuff, stdoutbuff bytes.Buffer
ctx, _ := plugin.NewContext(
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for messages.
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for status messages.
nil, // the host that can be used to fetch providers.
nil, // configSource
t.TempDir(), // the working directory to spawn all plugins in.
nil, // runtimeOptions
false, // disableProviderPreview
nil,
)
s := &ImportStep{
deployment: &Deployment{
opts: &Options{
DryRun: false, // DryRun false denotes up
},
olds: map[resource.URN]*resource.State{},
news: &gsync.Map[urn.URN, *resource.State]{},
ctx: ctx,
},
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{
DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResponse, error) {
diffCalled = true
return plugin.DiffResponse{
Changes: plugin.DiffSome,
ChangedKeys: []resource.PropertyKey{
resource.PropertyKey("non-matching-input-1"),
resource.PropertyKey("non-matching-input-2"),
},
}, nil
},
},
}
_, _, err := s.Apply()
assert.Error(t, err)
assert.True(t, diffCalled)
assert.Equal(t, 0, len(stderrbuff.String()))
assert.ErrorContains(t, err, "[non-matching-input-1 non-matching-input-2]")
})
t.Run("up: no resource input diff found", func(t *testing.T) {
t.Parallel()
var stderrbuff, stdoutbuff bytes.Buffer
ctx, _ := plugin.NewContext(
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for messages.
diagtest.MockSink(&stdoutbuff, &stderrbuff), // The diagnostics sink to use for status messages.
nil, // the host that can be used to fetch providers.
nil, // configSource
t.TempDir(), // the working directory to spawn all plugins in.
nil, // runtimeOptions
false, // disableProviderPreview
nil,
)
s := &ImportStep{
deployment: &Deployment{
opts: &Options{
DryRun: false, // DryRun false denotes up
},
olds: map[resource.URN]*resource.State{},
news: &gsync.Map[urn.URN, *resource.State]{},
ctx: ctx,
},
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{},
}
_, _, err := s.Apply()
assert.NoError(t, err)
assert.Equal(t, 0, len(stderrbuff.String()))
})
})
}
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))
}
}