pulumi/pkg/engine/lifeycletest/pulumi_test.go
Pat Gavlin 3d2e31289a
Add support for serialized resource references. (#5041)
Resources are serialized as their URN, ID, and package version. Each
Pulumi package is expected to register itself with the SDK. The package
will be invoked to construct appropriate instances of rehydrated
resources. Packages are distinguished by their name and their version.

This is the foundation of cross-process resources.

Related to #2430.

Co-authored-by: Mikhail Shilkov <github@mikhail.io>
Co-authored-by: Luke Hoban <luke@pulumi.com>
Co-authored-by: Levi Blackstone <levi@pulumi.com>
2020-10-27 10:12:12 -07:00

5691 lines
180 KiB
Go

// 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.
// nolint: goconst
package lifecycletest
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"testing"
"github.com/blang/semver"
pbempty "github.com/golang/protobuf/ptypes/empty"
combinations "github.com/mxschmitt/golang-combinations"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
. "github.com/pulumi/pulumi/pkg/v2/engine"
"github.com/pulumi/pulumi/pkg/v2/resource/deploy"
"github.com/pulumi/pulumi/pkg/v2/resource/deploy/deploytest"
"github.com/pulumi/pulumi/pkg/v2/resource/deploy/providers"
"github.com/pulumi/pulumi/sdk/v2/go/common/diag"
"github.com/pulumi/pulumi/sdk/v2/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/result"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/rpcutil"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/rpcutil/rpcerror"
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
pulumirpc "github.com/pulumi/pulumi/sdk/v2/proto/go"
)
func SuccessfulSteps(entries JournalEntries) []deploy.Step {
var steps []deploy.Step
for _, entry := range entries {
if entry.Kind == JournalEntrySuccess {
steps = append(steps, entry.Step)
}
}
return steps
}
type StepSummary struct {
Op deploy.StepOp
URN resource.URN
}
func AssertSameSteps(t *testing.T, expected []StepSummary, actual []deploy.Step) bool {
assert.Equal(t, len(expected), len(actual))
for _, exp := range expected {
act := actual[0]
actual = actual[1:]
if !assert.Equal(t, exp.Op, act.Op()) || !assert.Equal(t, exp.URN, act.URN()) {
return false
}
}
return true
}
func TestEmptyProgramLifecycle(t *testing.T) {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
return nil
})
host := deploytest.NewPluginHost(nil, nil, program)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: MakeBasicLifecycleSteps(t, 0),
}
p.Run(t, nil)
}
func TestSingleResourceDefaultProviderLifecycle(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: MakeBasicLifecycleSteps(t, 2),
}
p.Run(t, nil)
}
func TestSingleResourceExplicitProviderLifecycle(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
provURN, provID, _, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true)
assert.NoError(t, err)
if provID == "" {
provID = providers.UnknownID
}
provRef, err := providers.NewReference(provURN, provID)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Provider: provRef.String(),
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: MakeBasicLifecycleSteps(t, 2),
}
p.Run(t, nil)
}
func TestSingleResourceDefaultProviderUpgrade(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Create an old snapshot with an existing copy of the single resource and no providers.
old := &deploy.Snapshot{
Resources: []*resource.State{{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "0",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
}},
}
isRefresh := false
validate := func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
// Should see only sames: the default provider should be injected into the old state before the update
// runs.
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN, resURN:
expect := deploy.OpSame
if isRefresh {
expect = deploy.OpRefresh
}
assert.Equal(t, expect, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
assert.Len(t, entries.Snap(target.Snapshot).Resources, 2)
return res
}
// Run a single update step using the base snapshot.
p.Steps = []TestStep{{Op: Update, Validate: validate}}
p.Run(t, old)
// Run a single refresh step using the base snapshot.
isRefresh = true
p.Steps = []TestStep{{Op: Refresh, Validate: validate}}
p.Run(t, old)
// Run a single destroy step using the base snapshot.
isRefresh = false
p.Steps = []TestStep{{
Op: Destroy,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
// Should see two deletes: the default provider should be injected into the old state before the update
// runs.
deleted := make(map[resource.URN]bool)
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN, resURN:
deleted[urn] = true
assert.Equal(t, deploy.OpDelete, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
assert.Len(t, deleted, 2)
assert.Len(t, entries.Snap(target.Snapshot).Resources, 0)
return res
},
}}
p.Run(t, old)
// Run a partial lifecycle using the base snapshot, skipping the initial update step.
p.Steps = MakeBasicLifecycleSteps(t, 2)[1:]
p.Run(t, old)
}
func TestSingleResourceDefaultProviderReplace(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// Always require replacement.
keys := []resource.PropertyKey{}
for k := range news {
keys = append(keys, k)
}
return plugin.DiffResult{ReplaceKeys: keys}, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Config: config.Map{
config.MustMakeKey("pkgA", "foo"): config.NewValue("bar"),
},
}
// Build a basic lifecycle.
steps := MakeBasicLifecycleSteps(t, 2)
// Run the lifecycle through its no-op update+refresh.
p.Steps = steps[:4]
snap := p.Run(t, nil)
// Change the config and run an update. We expect everything to require replacement.
p.Config[config.MustMakeKey("pkgA", "foo")] = config.NewValue("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Look for replace steps on the provider and the resource.
replacedProvider, replacedResource := false, false
for _, entry := range entries {
if entry.Kind != JournalEntrySuccess || entry.Step.Op() != deploy.OpDeleteReplaced {
continue
}
switch urn := entry.Step.URN(); urn {
case provURN:
replacedProvider = true
case resURN:
replacedResource = true
default:
t.Fatalf("unexpected resource %v", urn)
}
}
assert.True(t, replacedProvider)
assert.True(t, replacedResource)
return res
},
}}
snap = p.Run(t, snap)
// Resume the lifecycle with another no-op update.
p.Steps = steps[2:]
p.Run(t, snap)
}
func TestSingleResourceExplicitProviderReplace(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// Always require replacement.
keys := []resource.PropertyKey{}
for k := range news {
keys = append(keys, k)
}
return plugin.DiffResult{ReplaceKeys: keys}, nil
},
}, nil
}),
}
providerInputs := resource.PropertyMap{
resource.PropertyKey("foo"): resource.NewStringProperty("bar"),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
provURN, provID, _, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true,
deploytest.ResourceOptions{Inputs: providerInputs})
assert.NoError(t, err)
if provID == "" {
provID = providers.UnknownID
}
provRef, err := providers.NewReference(provURN, provID)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Provider: provRef.String(),
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
// Build a basic lifecycle.
steps := MakeBasicLifecycleSteps(t, 2)
// Run the lifecycle through its no-op update+refresh.
p.Steps = steps[:4]
snap := p.Run(t, nil)
// Change the config and run an update. We expect everything to require replacement.
providerInputs[resource.PropertyKey("foo")] = resource.NewStringProperty("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
provURN := p.NewProviderURN("pkgA", "provA", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Look for replace steps on the provider and the resource.
replacedProvider, replacedResource := false, false
for _, entry := range entries {
if entry.Kind != JournalEntrySuccess || entry.Step.Op() != deploy.OpDeleteReplaced {
continue
}
switch urn := entry.Step.URN(); urn {
case provURN:
replacedProvider = true
case resURN:
replacedResource = true
default:
t.Fatalf("unexpected resource %v", urn)
}
}
assert.True(t, replacedProvider)
assert.True(t, replacedResource)
return res
},
}}
snap = p.Run(t, snap)
// Resume the lifecycle with another no-op update.
p.Steps = steps[2:]
p.Run(t, snap)
}
func TestSingleResourceExplicitProviderDeleteBeforeReplace(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// Always require replacement.
keys := []resource.PropertyKey{}
for k := range news {
keys = append(keys, k)
}
return plugin.DiffResult{ReplaceKeys: keys, DeleteBeforeReplace: true}, nil
},
}, nil
}),
}
providerInputs := resource.PropertyMap{
resource.PropertyKey("foo"): resource.NewStringProperty("bar"),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
provURN, provID, _, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true,
deploytest.ResourceOptions{Inputs: providerInputs})
assert.NoError(t, err)
if provID == "" {
provID = providers.UnknownID
}
provRef, err := providers.NewReference(provURN, provID)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Provider: provRef.String(),
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
// Build a basic lifecycle.
steps := MakeBasicLifecycleSteps(t, 2)
// Run the lifecycle through its no-op update+refresh.
p.Steps = steps[:4]
snap := p.Run(t, nil)
// Change the config and run an update. We expect everything to require replacement.
providerInputs[resource.PropertyKey("foo")] = resource.NewStringProperty("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
provURN := p.NewProviderURN("pkgA", "provA", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Look for replace steps on the provider and the resource.
createdProvider, createdResource := false, false
deletedProvider, deletedResource := false, false
for _, entry := range entries {
if entry.Kind != JournalEntrySuccess {
continue
}
switch urn := entry.Step.URN(); urn {
case provURN:
if entry.Step.Op() == deploy.OpDeleteReplaced {
assert.False(t, createdProvider)
assert.False(t, createdResource)
assert.True(t, deletedResource)
deletedProvider = true
} else if entry.Step.Op() == deploy.OpCreateReplacement {
assert.True(t, deletedProvider)
assert.True(t, deletedResource)
assert.False(t, createdResource)
createdProvider = true
}
case resURN:
if entry.Step.Op() == deploy.OpDeleteReplaced {
assert.False(t, deletedProvider)
assert.False(t, deletedResource)
deletedResource = true
} else if entry.Step.Op() == deploy.OpCreateReplacement {
assert.True(t, deletedProvider)
assert.True(t, deletedResource)
assert.True(t, createdProvider)
createdResource = true
}
default:
t.Fatalf("unexpected resource %v", urn)
}
}
assert.True(t, deletedProvider)
assert.True(t, deletedResource)
return res
},
}}
snap = p.Run(t, snap)
// Resume the lifecycle with another no-op update.
p.Steps = steps[2:]
p.Run(t, snap)
}
func TestSingleResourceDiffUnavailable(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
return plugin.DiffResult{}, plugin.DiffUnavailable("diff unavailable")
},
}, nil
}),
}
inputs := resource.PropertyMap{}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: inputs,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Run the initial update.
project := p.GetProject()
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
assert.Nil(t, res)
// Now change the inputs to our resource and run a preview.
inputs = resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
events []Event, res result.Result) result.Result {
found := false
for _, e := range events {
if e.Type == DiagEvent {
p := e.Payload().(DiagEventPayload)
if p.URN == resURN && p.Severity == diag.Warning && p.Message == "diff unavailable" {
found = true
break
}
}
}
assert.True(t, found)
return res
})
assert.Nil(t, res)
}
func TestDestroyWithPendingDelete(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Create an old snapshot with two copies of a resource that share a URN: one that is pending deletion and one
// that is not.
old := &deploy.Snapshot{
Resources: []*resource.State{
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "1",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
},
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "0",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
Delete: true,
},
},
}
p.Steps = []TestStep{{
Op: Update,
Validate: func(_ workspace.Project, _ deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
// Verify that we see a DeleteReplacement for the resource with ID 0 and a Delete for the resource with
// ID 1.
deletedID0, deletedID1 := false, false
for _, entry := range entries {
// Ignore non-terminal steps and steps that affect the injected default provider.
if entry.Kind != JournalEntrySuccess || entry.Step.URN() != resURN ||
(entry.Step.Op() != deploy.OpDelete && entry.Step.Op() != deploy.OpDeleteReplaced) {
continue
}
switch id := entry.Step.Old().ID; id {
case "0":
assert.False(t, deletedID0)
deletedID0 = true
case "1":
assert.False(t, deletedID1)
deletedID1 = true
default:
assert.Fail(t, "unexpected resource ID %v", string(id))
}
}
assert.True(t, deletedID0)
assert.True(t, deletedID1)
return res
},
}}
p.Run(t, old)
}
func TestUpdateWithPendingDelete(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
host := deploytest.NewPluginHost(nil, nil, nil, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Create an old snapshot with two copies of a resource that share a URN: one that is pending deletion and one
// that is not.
old := &deploy.Snapshot{
Resources: []*resource.State{
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "1",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
},
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "0",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
Delete: true,
},
},
}
p.Steps = []TestStep{{
Op: Destroy,
Validate: func(_ workspace.Project, _ deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
// Verify that we see a DeleteReplacement for the resource with ID 0 and a Delete for the resource with
// ID 1.
deletedID0, deletedID1 := false, false
for _, entry := range entries {
// Ignore non-terminal steps and steps that affect the injected default provider.
if entry.Kind != JournalEntrySuccess || entry.Step.URN() != resURN ||
(entry.Step.Op() != deploy.OpDelete && entry.Step.Op() != deploy.OpDeleteReplaced) {
continue
}
switch id := entry.Step.Old().ID; id {
case "0":
assert.False(t, deletedID0)
deletedID0 = true
case "1":
assert.False(t, deletedID1)
deletedID1 = true
default:
assert.Fail(t, "unexpected resource ID %v", string(id))
}
}
assert.True(t, deletedID0)
assert.True(t, deletedID1)
return res
},
}}
p.Run(t, old)
}
func TestParallelRefresh(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
// Create a program that registers four resources, each of which depends on the resource that immediately precedes
// it.
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
resA, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
resB, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
Dependencies: []resource.URN{resA},
})
assert.NoError(t, err)
resC, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
Dependencies: []resource.URN{resB},
})
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resD", true, deploytest.ResourceOptions{
Dependencies: []resource.URN{resC},
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Parallel: 4, Host: host},
}
p.Steps = []TestStep{{Op: Update}}
snap := p.Run(t, nil)
assert.Len(t, snap.Resources, 5)
assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
assert.Equal(t, string(snap.Resources[2].URN.Name()), "resB")
assert.Equal(t, string(snap.Resources[3].URN.Name()), "resC")
assert.Equal(t, string(snap.Resources[4].URN.Name()), "resD")
p.Steps = []TestStep{{Op: Refresh}}
snap = p.Run(t, snap)
assert.Len(t, snap.Resources, 5)
assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
assert.Equal(t, string(snap.Resources[2].URN.Name()), "resB")
assert.Equal(t, string(snap.Resources[3].URN.Name()), "resC")
assert.Equal(t, string(snap.Resources[4].URN.Name()), "resD")
}
func TestExternalRefresh(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
// Our program reads a resource and exits.
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "resA-some-id", "", resource.PropertyMap{}, "", "")
if !assert.NoError(t, err) {
t.FailNow()
}
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{Op: Update}},
}
// The read should place "resA" in the snapshot with the "External" bit set.
snap := p.Run(t, nil)
assert.Len(t, snap.Resources, 2)
assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
assert.True(t, snap.Resources[1].External)
p = &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{Op: Refresh}},
}
snap = p.Run(t, snap)
// A refresh should leave "resA" as it is in the snapshot. The External bit should still be set.
assert.Len(t, snap.Resources, 2)
assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
assert.True(t, snap.Resources[1].External)
}
func TestRefreshInitFailure(t *testing.T) {
p := &TestPlan{}
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
res2URN := p.NewURN("pkgA:m:typA", "resB", "")
res2Outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
//
// Refresh will persist any initialization errors that are returned by `Read`. This provider
// will error out or not based on the value of `refreshShouldFail`.
//
refreshShouldFail := false
//
// Set up test environment to use `readFailProvider` as the underlying resource provider.
//
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
) (plugin.ReadResult, resource.Status, error) {
if refreshShouldFail && urn == resURN {
err := &plugin.InitError{
Reasons: []string{"Refresh reports continued to fail to initialize"},
}
return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusPartialFailure, err
} else if urn == res2URN {
return plugin.ReadResult{Outputs: res2Outputs}, resource.StatusOK, nil
}
return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusOK, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Options.Host = host
//
// Create an old snapshot with a single initialization failure.
//
old := &deploy.Snapshot{
Resources: []*resource.State{
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "0",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
InitErrors: []string{"Resource failed to initialize"},
},
{
Type: res2URN.Type(),
URN: res2URN,
Custom: true,
ID: "1",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
},
},
}
//
// Refresh DOES NOT fail, causing the initialization error to disappear.
//
p.Steps = []TestStep{{Op: Refresh}}
snap := p.Run(t, old)
for _, resource := range snap.Resources {
switch urn := resource.URN; urn {
case provURN:
// break
case resURN:
assert.Empty(t, resource.InitErrors)
case res2URN:
assert.Equal(t, res2Outputs, resource.Outputs)
default:
t.Fatalf("unexpected resource %v", urn)
}
}
//
// Refresh again, see the resource is in a partial state of failure, but the refresh operation
// DOES NOT fail. The initialization error is still persisted.
//
refreshShouldFail = true
p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}
snap = p.Run(t, old)
for _, resource := range snap.Resources {
switch urn := resource.URN; urn {
case provURN:
// break
case resURN:
assert.Equal(t, []string{"Refresh reports continued to fail to initialize"}, resource.InitErrors)
case res2URN:
assert.Equal(t, res2Outputs, resource.Outputs)
default:
t.Fatalf("unexpected resource %v", urn)
}
}
}
// Test that ensures that we log diagnostics for resources that receive an error from Check. (Note that this
// is distinct from receiving non-error failures from Check.)
func TestCheckFailureRecord(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CheckF: func(urn resource.URN,
olds, news resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
return nil, nil, errors.New("oh no, check had an error")
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.Error(t, err)
return err
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
sawFailure := false
for _, evt := range evts {
if evt.Type == DiagEvent {
e := evt.Payload().(DiagEventPayload)
msg := colors.Never.Colorize(e.Message)
sawFailure = msg == "oh no, check had an error\n" && e.Severity == diag.Error
}
}
assert.True(t, sawFailure)
return res
},
}},
}
p.Run(t, nil)
}
// Test that checks that we emit diagnostics for properties that check says are invalid.
func TestCheckFailureInvalidPropertyRecord(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CheckF: func(urn resource.URN,
olds, news resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
return nil, []plugin.CheckFailure{{
Property: "someprop",
Reason: "field is not valid",
}}, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.Error(t, err)
return err
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
sawFailure := false
for _, evt := range evts {
if evt.Type == DiagEvent {
e := evt.Payload().(DiagEventPayload)
msg := colors.Never.Colorize(e.Message)
sawFailure = strings.Contains(msg, "field is not valid") && e.Severity == diag.Error
if sawFailure {
break
}
}
}
assert.True(t, sawFailure)
return res
},
}},
}
p.Run(t, nil)
}
// Test that tests that Refresh can detect that resources have been deleted and removes them
// from the snapshot.
func TestRefreshWithDelete(t *testing.T) {
for _, parallelFactor := range []int{1, 4} {
t.Run(fmt.Sprintf("parallel-%d", parallelFactor), func(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
) (plugin.ReadResult, resource.Status, error) {
// This thing doesn't exist. Returning nil from Read should trigger
// the engine to delete it from the snapshot.
return plugin.ReadResult{}, resource.StatusOK, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return err
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{Options: UpdateOptions{Host: host, Parallel: parallelFactor}}
p.Steps = []TestStep{{Op: Update}}
snap := p.Run(t, nil)
p.Steps = []TestStep{{Op: Refresh}}
snap = p.Run(t, snap)
// Refresh succeeds and records that the resource in the snapshot doesn't exist anymore
provURN := p.NewProviderURN("pkgA", "default", "")
assert.Len(t, snap.Resources, 1)
assert.Equal(t, provURN, snap.Resources[0].URN)
})
}
}
func pickURN(t *testing.T, urns []resource.URN, names []string, target string) resource.URN {
assert.Equal(t, len(urns), len(names))
assert.Contains(t, names, target)
for i, name := range names {
if name == target {
return urns[i]
}
}
t.Fatalf("Could not find target: %v in %v", target, names)
return ""
}
// Tests that dependencies are correctly rewritten when refresh removes deleted resources.
func TestRefreshDeleteDependencies(t *testing.T) {
names := []string{"resA", "resB", "resC"}
// Try refreshing a stack with every combination of the three above resources as a target to
// refresh.
subsets := combinations.All(names)
// combinations.All doesn't return the empty set. So explicitly test that case (i.e. test no
// targets specified)
validateRefreshDeleteCombination(t, names, []string{})
for _, subset := range subsets {
validateRefreshDeleteCombination(t, names, subset)
}
}
func validateRefreshDeleteCombination(t *testing.T, names []string, targets []string) {
p := &TestPlan{}
const resType = "pkgA:m:typA"
urnA := p.NewURN(resType, names[0], "")
urnB := p.NewURN(resType, names[1], "")
urnC := p.NewURN(resType, names[2], "")
urns := []resource.URN{urnA, urnB, urnC}
refreshTargets := []resource.URN{}
t.Logf("Refreshing targets: %v", targets)
for _, target := range targets {
refreshTargets = append(refreshTargets, pickURN(t, urns, names, target))
}
p.Options.RefreshTargets = refreshTargets
newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
return &resource.State{
Type: urn.Type(),
URN: urn,
Custom: true,
Delete: delete,
ID: id,
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
Dependencies: dependencies,
}
}
oldResources := []*resource.State{
newResource(urnA, "0", false),
newResource(urnB, "1", false, urnA),
newResource(urnC, "2", false, urnA, urnB),
newResource(urnA, "3", true),
newResource(urnA, "4", true),
newResource(urnC, "5", true, urnA, urnB),
}
old := &deploy.Snapshot{
Resources: oldResources,
}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
switch id {
case "0", "4":
// We want to delete resources A::0 and A::4.
return plugin.ReadResult{}, resource.StatusOK, nil
default:
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
}
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, nil, loaders...)
p.Steps = []TestStep{
{
Op: Refresh,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
// Should see only refreshes.
for _, entry := range entries {
if len(refreshTargets) > 0 {
// should only see changes to urns we explicitly asked to change
assert.Containsf(t, refreshTargets, entry.Step.URN(),
"Refreshed a resource that wasn't a target: %v", entry.Step.URN())
}
assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
}
return res
},
},
}
snap := p.Run(t, old)
provURN := p.NewProviderURN("pkgA", "default", "")
for _, r := range snap.Resources {
switch urn := r.URN; urn {
case provURN:
continue
case urnA, urnB, urnC:
// break
default:
t.Fatalf("unexpected resource %v", urn)
}
if len(refreshTargets) == 0 || containsURN(refreshTargets, urnA) {
// 'A' was deleted, so we should see the impact downstream.
switch r.ID {
case "1":
// A::0 was deleted, so B's dependency list should be empty.
assert.Equal(t, urnB, r.URN)
assert.Empty(t, r.Dependencies)
case "2":
// A::0 was deleted, so C's dependency list should only contain B.
assert.Equal(t, urnC, r.URN)
assert.Equal(t, []resource.URN{urnB}, r.Dependencies)
case "3":
// A::3 should not have changed.
assert.Equal(t, oldResources[3], r)
case "5":
// A::4 was deleted but A::3 was still refernceable by C, so C should not have changed.
assert.Equal(t, oldResources[5], r)
default:
t.Fatalf("Unexpected changed resource when refreshing %v: %v::%v", refreshTargets, r.URN, r.ID)
}
} else {
// A was not deleted. So nothing should be impacted.
id, err := strconv.Atoi(r.ID.String())
assert.NoError(t, err)
assert.Equal(t, oldResources[id], r)
}
}
}
func containsURN(urns []resource.URN, urn resource.URN) bool {
for _, val := range urns {
if val == urn {
return true
}
}
return false
}
// Tests basic refresh functionality.
func TestRefreshBasics(t *testing.T) {
names := []string{"resA", "resB", "resC"}
// Try refreshing a stack with every combination of the three above resources as a target to
// refresh.
subsets := combinations.All(names)
// combinations.All doesn't return the empty set. So explicitly test that case (i.e. test no
// targets specified)
validateRefreshBasicsCombination(t, names, []string{})
for _, subset := range subsets {
validateRefreshBasicsCombination(t, names, subset)
}
}
func validateRefreshBasicsCombination(t *testing.T, names []string, targets []string) {
p := &TestPlan{}
const resType = "pkgA:m:typA"
urnA := p.NewURN(resType, names[0], "")
urnB := p.NewURN(resType, names[1], "")
urnC := p.NewURN(resType, names[2], "")
urns := []resource.URN{urnA, urnB, urnC}
refreshTargets := []resource.URN{}
for _, target := range targets {
refreshTargets = append(p.Options.RefreshTargets, pickURN(t, urns, names, target))
}
p.Options.RefreshTargets = refreshTargets
newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
return &resource.State{
Type: urn.Type(),
URN: urn,
Custom: true,
Delete: delete,
ID: id,
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
Dependencies: dependencies,
}
}
oldResources := []*resource.State{
newResource(urnA, "0", false),
newResource(urnB, "1", false, urnA),
newResource(urnC, "2", false, urnA, urnB),
newResource(urnA, "3", true),
newResource(urnA, "4", true),
newResource(urnC, "5", true, urnA, urnB),
}
newStates := map[resource.ID]plugin.ReadResult{
// A::0 and A::3 will have no changes.
"0": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}},
"3": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}},
// B::1 and A::4 will have changes. The latter will also have input changes.
"1": {Outputs: resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, Inputs: resource.PropertyMap{}},
"4": {
Outputs: resource.PropertyMap{"baz": resource.NewStringProperty("qux")},
Inputs: resource.PropertyMap{"oof": resource.NewStringProperty("zab")},
},
// C::2 and C::5 will be deleted.
"2": {},
"5": {},
}
old := &deploy.Snapshot{
Resources: oldResources,
}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
new, hasNewState := newStates[id]
assert.True(t, hasNewState)
return new, resource.StatusOK, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, nil, loaders...)
p.Steps = []TestStep{{
Op: Refresh,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
// Should see only refreshes.
for _, entry := range entries {
if len(refreshTargets) > 0 {
// should only see changes to urns we explicitly asked to change
assert.Containsf(t, refreshTargets, entry.Step.URN(),
"Refreshed a resource that wasn't a target: %v", entry.Step.URN())
}
assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
resultOp := entry.Step.(*deploy.RefreshStep).ResultOp()
old := entry.Step.Old()
if !old.Custom || providers.IsProviderType(old.Type) {
// Component and provider resources should never change.
assert.Equal(t, deploy.OpSame, resultOp)
continue
}
expected, new := newStates[old.ID], entry.Step.New()
if expected.Outputs == nil {
// If the resource was deleted, we want the result op to be an OpDelete.
assert.Nil(t, new)
assert.Equal(t, deploy.OpDelete, resultOp)
} else {
// If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want
// an OpSame.
if reflect.DeepEqual(old.Outputs, expected.Outputs) {
assert.Equal(t, deploy.OpSame, resultOp)
} else {
assert.Equal(t, deploy.OpUpdate, resultOp)
}
// Only the inputs and outputs should have changed (if anything changed).
old.Inputs = expected.Inputs
old.Outputs = expected.Outputs
assert.Equal(t, old, new)
}
}
return res
},
}}
snap := p.Run(t, old)
provURN := p.NewProviderURN("pkgA", "default", "")
for _, r := range snap.Resources {
switch urn := r.URN; urn {
case provURN:
continue
case urnA, urnB, urnC:
// break
default:
t.Fatalf("unexpected resource %v", urn)
}
// The only resources left in the checkpoint should be those that were not deleted by the refresh.
expected := newStates[r.ID]
assert.NotNil(t, expected)
idx, err := strconv.ParseInt(string(r.ID), 0, 0)
assert.NoError(t, err)
// The new resources should be equal to the old resources + the new inputs and outputs.
old := oldResources[int(idx)]
old.Inputs = expected.Inputs
old.Outputs = expected.Outputs
assert.Equal(t, old, r)
}
}
// Tests that an interrupted refresh leaves behind an expected state.
func TestCanceledRefresh(t *testing.T) {
p := &TestPlan{}
const resType = "pkgA:m:typA"
urnA := p.NewURN(resType, "resA", "")
urnB := p.NewURN(resType, "resB", "")
urnC := p.NewURN(resType, "resC", "")
newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
return &resource.State{
Type: urn.Type(),
URN: urn,
Custom: true,
Delete: delete,
ID: id,
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
Dependencies: dependencies,
}
}
oldResources := []*resource.State{
newResource(urnA, "0", false),
newResource(urnB, "1", false),
newResource(urnC, "2", false),
}
newStates := map[resource.ID]resource.PropertyMap{
// A::0 and B::1 will have changes; D::3 will be deleted.
"0": {"foo": resource.NewStringProperty("bar")},
"1": {"baz": resource.NewStringProperty("qux")},
"2": nil,
}
old := &deploy.Snapshot{
Resources: oldResources,
}
// Set up a cancelable context for the refresh operation.
ctx, cancel := context.WithCancel(context.Background())
// Serialize all refreshes s.t. we can cancel after the first is issued.
refreshes, cancelled := make(chan resource.ID), make(chan bool)
go func() {
<-refreshes
cancel()
}()
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
refreshes <- id
<-cancelled
new, hasNewState := newStates[id]
assert.True(t, hasNewState)
return plugin.ReadResult{Outputs: new}, resource.StatusOK, nil
},
CancelF: func() error {
close(cancelled)
return nil
},
}, nil
}),
}
refreshed := make(map[resource.ID]bool)
op := TestOp(Refresh)
options := UpdateOptions{
Parallel: 1,
Host: deploytest.NewPluginHost(nil, nil, nil, loaders...),
}
project, target := p.GetProject(), p.GetTarget(old)
validate := func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
for _, entry := range entries {
assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
resultOp := entry.Step.(*deploy.RefreshStep).ResultOp()
old := entry.Step.Old()
if !old.Custom || providers.IsProviderType(old.Type) {
// Component and provider resources should never change.
assert.Equal(t, deploy.OpSame, resultOp)
continue
}
refreshed[old.ID] = true
expected, new := newStates[old.ID], entry.Step.New()
if expected == nil {
// If the resource was deleted, we want the result op to be an OpDelete.
assert.Nil(t, new)
assert.Equal(t, deploy.OpDelete, resultOp)
} else {
// If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want
// an OpSame.
if reflect.DeepEqual(old.Outputs, expected) {
assert.Equal(t, deploy.OpSame, resultOp)
} else {
assert.Equal(t, deploy.OpUpdate, resultOp)
}
// Only the outputs should have changed (if anything changed).
old.Outputs = expected
assert.Equal(t, old, new)
}
}
return res
}
snap, res := op.RunWithContext(ctx, project, target, options, false, nil, validate)
assertIsErrorOrBailResult(t, res)
assert.Equal(t, 1, len(refreshed))
provURN := p.NewProviderURN("pkgA", "default", "")
for _, r := range snap.Resources {
switch urn := r.URN; urn {
case provURN:
continue
case urnA, urnB, urnC:
// break
default:
t.Fatalf("unexpected resource %v", urn)
}
idx, err := strconv.ParseInt(string(r.ID), 0, 0)
assert.NoError(t, err)
if refreshed[r.ID] {
// The refreshed resource should have its new state.
expected := newStates[r.ID]
if expected == nil {
assert.Fail(t, "refreshed resource was not deleted")
} else {
old := oldResources[int(idx)]
old.Outputs = expected
assert.Equal(t, old, r)
}
} else {
// Any resources that were not refreshed should retain their original state.
old := oldResources[int(idx)]
assert.Equal(t, old, r)
}
}
}
// Tests that errors returned directly from the language host get logged by the engine.
func TestLanguageHostDiagnostics(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
errorText := "oh no"
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
// Exiting immediately with an error simulates a language exiting immediately with a non-zero exit code.
return errors.New(errorText)
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assertIsErrorOrBailResult(t, res)
sawExitCode := false
for _, evt := range evts {
if evt.Type == DiagEvent {
e := evt.Payload().(DiagEventPayload)
msg := colors.Never.Colorize(e.Message)
sawExitCode = strings.Contains(msg, errorText) && e.Severity == diag.Error
if sawExitCode {
break
}
}
}
assert.True(t, sawExitCode)
return res
},
}},
}
p.Run(t, nil)
}
type brokenDecrypter struct {
ErrorMessage string
}
func (b brokenDecrypter) DecryptValue(ciphertext string) (string, error) {
return "", fmt.Errorf(b.ErrorMessage)
}
// Tests that the engine presents a reasonable error message when a decrypter fails to decrypt a config value.
func TestBrokenDecrypter(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
key := config.MustMakeKey("foo", "bar")
msg := "decryption failed"
configMap := make(config.Map)
configMap[key] = config.NewSecureValue("hunter2")
p := &TestPlan{
Options: UpdateOptions{Host: host},
Decrypter: brokenDecrypter{ErrorMessage: msg},
Config: configMap,
Steps: []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assertIsErrorOrBailResult(t, res)
decryptErr := res.Error().(DecryptError)
assert.Equal(t, key, decryptErr.Key)
assert.Contains(t, decryptErr.Err.Error(), msg)
return res
},
}},
}
p.Run(t, nil)
}
func TestBadResourceType(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
_, _, _, err := mon.RegisterResource("very:bad", "resA", true)
assert.Error(t, err)
rpcerr, ok := rpcerror.FromError(err)
assert.True(t, ok)
assert.Equal(t, codes.InvalidArgument, rpcerr.Code())
assert.Contains(t, rpcerr.Message(), "Type 'very:bad' is not a valid type token")
_, _, err = mon.ReadResource("very:bad", "someResource", "someId", "", resource.PropertyMap{}, "", "")
assert.Error(t, err)
rpcerr, ok = rpcerror.FromError(err)
assert.True(t, ok)
assert.Equal(t, codes.InvalidArgument, rpcerr.Code())
assert.Contains(t, rpcerr.Message(), "Type 'very:bad' is not a valid type token")
// Component resources may have any format type.
_, _, _, noErr := mon.RegisterResource("a:component", "resB", false)
assert.NoError(t, noErr)
_, _, _, noErr = mon.RegisterResource("singlename", "resC", false)
assert.NoError(t, noErr)
return err
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
}},
}
p.Run(t, nil)
}
// Tests that provider cancellation occurs as expected.
func TestProviderCancellation(t *testing.T) {
const resourceCount = 4
// Set up a cancelable context for the refresh operation.
ctx, cancel := context.WithCancel(context.Background())
// Wait for our resource ops, then cancel.
var ops sync.WaitGroup
ops.Add(resourceCount)
go func() {
ops.Wait()
cancel()
}()
// Set up an independent cancelable context for the provider's operations.
provCtx, provCancel := context.WithCancel(context.Background())
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
// Inform the waiter that we've entered a provider op and wait for cancellation.
ops.Done()
<-provCtx.Done()
return resource.ID(urn.Name()), resource.PropertyMap{}, resource.StatusOK, nil
},
CancelF: func() error {
provCancel()
return nil
},
}, nil
}),
}
done := make(chan bool)
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
errors := make([]error, resourceCount)
var resources sync.WaitGroup
resources.Add(resourceCount)
for i := 0; i < resourceCount; i++ {
go func(idx int) {
_, _, _, errors[idx] = monitor.RegisterResource("pkgA:m:typA", fmt.Sprintf("res%d", idx), true)
resources.Done()
}(i)
}
resources.Wait()
for _, err := range errors {
assert.NoError(t, err)
}
close(done)
return nil
})
p := &TestPlan{}
op := TestOp(Update)
options := UpdateOptions{
Parallel: resourceCount,
Host: deploytest.NewPluginHost(nil, nil, program, loaders...),
}
project, target := p.GetProject(), p.GetTarget(nil)
_, res := op.RunWithContext(ctx, project, target, options, false, nil, nil)
assertIsErrorOrBailResult(t, res)
// Wait for the program to finish.
<-done
}
// Tests that a preview works for a stack with pending operations.
func TestPreviewWithPendingOperations(t *testing.T) {
p := &TestPlan{}
const resType = "pkgA:m:typA"
urnA := p.NewURN(resType, "resA", "")
newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
return &resource.State{
Type: urn.Type(),
URN: urn,
Custom: true,
Delete: delete,
ID: id,
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
Dependencies: dependencies,
}
}
old := &deploy.Snapshot{
PendingOperations: []resource.Operation{{
Resource: newResource(urnA, "0", false),
Type: resource.OperationTypeUpdating,
}},
Resources: []*resource.State{
newResource(urnA, "0", false),
},
}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
op := TestOp(Update)
options := UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)}
project, target := p.GetProject(), p.GetTarget(old)
// A preview should succeed despite the pending operations.
_, res := op.Run(project, target, options, true, nil, nil)
assert.Nil(t, res)
// But an update should fail.
_, res = op.Run(project, target, options, false, nil, nil)
assertIsErrorOrBailResult(t, res)
assert.EqualError(t, res.Error(), deploy.PlanPendingOperationsError{}.Error())
}
// Tests that a failed partial update causes the engine to persist the resource's old inputs and new outputs.
func TestUpdatePartialFailure(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
return plugin.DiffResult{
Changes: plugin.DiffSome,
}, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
outputs := resource.NewPropertyMapFromMap(map[string]interface{}{
"output_prop": 42,
})
return outputs, resource.StatusPartialFailure, errors.New("update failed to apply")
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
_, _, _, err := mon.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"input_prop": "new inputs",
}),
})
return err
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{Options: UpdateOptions{Host: host}}
resURN := p.NewURN("pkgA:m:typA", "resA", "")
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assertIsErrorOrBailResult(t, res)
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case resURN:
assert.Equal(t, deploy.OpUpdate, entry.Step.Op())
switch entry.Kind {
case JournalEntryBegin:
continue
case JournalEntrySuccess:
inputs := entry.Step.New().Inputs
outputs := entry.Step.New().Outputs
assert.Len(t, inputs, 1)
assert.Len(t, outputs, 1)
assert.Equal(t,
resource.NewStringProperty("old inputs"), inputs[resource.PropertyKey("input_prop")])
assert.Equal(t,
resource.NewNumberProperty(42), outputs[resource.PropertyKey("output_prop")])
default:
t.Fatalf("unexpected journal operation: %d", entry.Kind)
}
}
}
return res
},
}}
old := &deploy.Snapshot{
Resources: []*resource.State{
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "1",
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"input_prop": "old inputs",
}),
Outputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"output_prop": 1,
}),
},
},
}
p.Run(t, old)
}
// Tests that the StackReference resource works as intended,
func TestStackReference(t *testing.T) {
loaders := []*deploytest.ProviderLoader{}
// Test that the normal lifecycle works correctly.
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
_, _, state, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"name": "other",
}),
})
assert.NoError(t, err)
if !info.DryRun {
assert.Equal(t, "bar", state["outputs"].ObjectValue()["foo"].StringValue())
}
return nil
})
p := &TestPlan{
BackendClient: &deploytest.BackendClient{
GetStackOutputsF: func(ctx context.Context, name string) (resource.PropertyMap, error) {
switch name {
case "other":
return resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "bar",
}), nil
default:
return nil, errors.Errorf("unknown stack \"%s\"", name)
}
},
},
Options: UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)},
Steps: MakeBasicLifecycleSteps(t, 2),
}
p.Run(t, nil)
// Test that changes to `name` cause replacement.
resURN := p.NewURN("pulumi:pulumi:StackReference", "other", "")
old := &deploy.Snapshot{
Resources: []*resource.State{
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "1",
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"name": "other2",
}),
Outputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"name": "other2",
"outputs": resource.PropertyMap{},
}),
},
},
}
p.Steps = []TestStep{{
Op: Update,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case resURN:
switch entry.Step.Op() {
case deploy.OpCreateReplacement, deploy.OpDeleteReplaced, deploy.OpReplace:
// OK
default:
t.Fatalf("unexpected journal operation: %v", entry.Step.Op())
}
}
}
return res
},
}}
p.Run(t, old)
// Test that unknown stacks are handled appropriately.
program = deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
_, _, _, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"name": "rehto",
}),
})
assert.Error(t, err)
return err
})
p.Options = UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: true,
SkipPreview: true,
}}
p.Run(t, nil)
// Test that unknown properties cause errors.
program = deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
_, _, _, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"name": "other",
"foo": "bar",
}),
})
assert.Error(t, err)
return err
})
p.Options = UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)}
p.Run(t, nil)
}
type channelWriter struct {
channel chan []byte
}
func (cw *channelWriter) Write(d []byte) (int, error) {
cw.channel <- d
return len(d), nil
}
// Tests that a failed plugin load correctly shuts down the host.
func TestLoadFailureShutdown(t *testing.T) {
// Note that the setup here is a bit baroque, and is intended to replicate the CLI architecture that lead to
// issue #2170. That issue--a panic on a closed channel--was caused by the intersection of several design choices:
//
// - The provider registry loads and configures the set of providers necessary for the resources currently in the
// checkpoint it is processing at plan creation time. Registry creation fails promptly if a provider plugin
// fails to load (e.g. because is binary is missing).
// - Provider configuration in the CLI's host happens asynchronously. This is meant to allow the engine to remain
// responsive while plugins configure.
// - Providers may call back into the CLI's host for logging. Callbacks are processed as long as the CLI's plugin
// context is open.
// - Log events from the CLI's host are delivered to the CLI's diagnostic streams via channels. The CLI closes
// these channels once the engine operation it initiated completes.
//
// These choices gave rise to the following situation:
// 1. The provider registry loads a provider for package A and kicks off its configuration.
// 2. The provider registry attempts to load a provider for package B. The load fails, and the provider registry
// creation call fails promptly.
// 3. The engine operation requested by the CLI fails promptly because provider registry creation failed.
// 4. The CLI shuts down its diagnostic channels.
// 5. The provider for package A calls back in to the host to log a message. The host then attempts to deliver
// the message to the CLI's diagnostic channels, causing a panic.
//
// The fix was to properly close the plugin host during step (3) s.t. the host was no longer accepting callbacks
// and would not attempt to send messages to the CLI's diagnostic channels.
//
// As such, this test attempts to replicate the CLI architecture by using one provider that configures
// asynchronously and attempts to call back into the engine and a second provider that fails to load.
release, done := make(chan bool), make(chan bool)
sinkWriter := &channelWriter{channel: make(chan []byte)}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoaderWithHost("pkgA", semver.MustParse("1.0.0"),
func(host plugin.Host) (plugin.Provider, error) {
return &deploytest.Provider{
ConfigureF: func(news resource.PropertyMap) error {
go func() {
<-release
host.Log(diag.Info, "", "configuring pkgA provider...", 0)
close(done)
}()
return nil
},
}, nil
}),
deploytest.NewProviderLoader("pkgB", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return nil, errors.New("pkgB load failure")
}),
}
p := &TestPlan{}
provAURN := p.NewProviderURN("pkgA", "default", "")
provBURN := p.NewProviderURN("pkgB", "default", "")
old := &deploy.Snapshot{
Resources: []*resource.State{
{
Type: provAURN.Type(),
URN: provAURN,
Custom: true,
ID: "0",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
},
{
Type: provBURN.Type(),
URN: provBURN,
Custom: true,
ID: "1",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
},
},
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
return nil
})
op := TestOp(Update)
sink := diag.DefaultSink(sinkWriter, sinkWriter, diag.FormatOptions{Color: colors.Raw})
options := UpdateOptions{Host: deploytest.NewPluginHost(sink, sink, program, loaders...)}
project, target := p.GetProject(), p.GetTarget(old)
_, res := op.Run(project, target, options, true, nil, nil)
assertIsErrorOrBailResult(t, res)
close(sinkWriter.channel)
close(release)
<-done
}
var complexTestDependencyGraphNames = []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"}
func generateComplexTestDependencyGraph(
t *testing.T, p *TestPlan) ([]resource.URN, *deploy.Snapshot, plugin.LanguageRuntime) {
resType := tokens.Type("pkgA:m:typA")
type propertyDependencies map[resource.PropertyKey][]resource.URN
names := complexTestDependencyGraphNames
urnA := p.NewProviderURN("pkgA", names[0], "")
urnB := p.NewURN(resType, names[1], "")
urnC := p.NewProviderURN("pkgA", names[2], "")
urnD := p.NewProviderURN("pkgA", names[3], "")
urnE := p.NewURN(resType, names[4], "")
urnF := p.NewURN(resType, names[5], "")
urnG := p.NewURN(resType, names[6], "")
urnH := p.NewURN(resType, names[7], "")
urnI := p.NewURN(resType, names[8], "")
urnJ := p.NewURN(resType, names[9], "")
urnK := p.NewURN(resType, names[10], "")
urnL := p.NewURN(resType, names[11], "")
urns := []resource.URN{
urnA, urnB, urnC, urnD, urnE, urnF,
urnG, urnH, urnI, urnJ, urnK, urnL,
}
newResource := func(urn resource.URN, id resource.ID, provider string, dependencies []resource.URN,
propertyDeps propertyDependencies, outputs resource.PropertyMap) *resource.State {
inputs := resource.PropertyMap{}
for k := range propertyDeps {
inputs[k] = resource.NewStringProperty("foo")
}
return &resource.State{
Type: urn.Type(),
URN: urn,
Custom: true,
Delete: false,
ID: id,
Inputs: inputs,
Outputs: outputs,
Dependencies: dependencies,
Provider: provider,
PropertyDependencies: propertyDeps,
}
}
old := &deploy.Snapshot{
Resources: []*resource.State{
newResource(urnA, "0", "", nil, nil, resource.PropertyMap{"A": resource.NewStringProperty("foo")}),
newResource(urnB, "1", string(urnA)+"::0", nil, nil, nil),
newResource(urnC, "2", "",
[]resource.URN{urnA},
propertyDependencies{"A": []resource.URN{urnA}},
resource.PropertyMap{"A": resource.NewStringProperty("bar")}),
newResource(urnD, "3", "",
[]resource.URN{urnA},
propertyDependencies{"B": []resource.URN{urnA}}, nil),
newResource(urnE, "4", string(urnC)+"::2", nil, nil, nil),
newResource(urnF, "5", "",
[]resource.URN{urnC},
propertyDependencies{"A": []resource.URN{urnC}}, nil),
newResource(urnG, "6", "",
[]resource.URN{urnC},
propertyDependencies{"B": []resource.URN{urnC}}, nil),
newResource(urnH, "4", string(urnD)+"::3", nil, nil, nil),
newResource(urnI, "5", "",
[]resource.URN{urnD},
propertyDependencies{"A": []resource.URN{urnD}}, nil),
newResource(urnJ, "6", "",
[]resource.URN{urnD},
propertyDependencies{"B": []resource.URN{urnD}}, nil),
newResource(urnK, "7", "",
[]resource.URN{urnF, urnG},
propertyDependencies{"A": []resource.URN{urnF, urnG}}, nil),
newResource(urnL, "8", "",
[]resource.URN{urnF, urnG},
propertyDependencies{"B": []resource.URN{urnF, urnG}}, nil),
},
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
register := func(urn resource.URN, provider string, inputs resource.PropertyMap) resource.ID {
_, id, _, err := monitor.RegisterResource(urn.Type(), string(urn.Name()), true, deploytest.ResourceOptions{
Provider: provider,
Inputs: inputs,
})
assert.NoError(t, err)
return id
}
idA := register(urnA, "", resource.PropertyMap{"A": resource.NewStringProperty("bar")})
register(urnB, string(urnA)+"::"+string(idA), nil)
idC := register(urnC, "", nil)
idD := register(urnD, "", nil)
register(urnE, string(urnC)+"::"+string(idC), nil)
register(urnF, "", nil)
register(urnG, "", nil)
register(urnH, string(urnD)+"::"+string(idD), nil)
register(urnI, "", nil)
register(urnJ, "", nil)
register(urnK, "", nil)
register(urnL, "", nil)
return nil
})
return urns, old, program
}
func TestDeleteBeforeReplace(t *testing.T) {
// A
// _________|_________
// B C D
// ___|___ ___|___
// E F G H I J
// |__|
// K L
//
// For a given resource R in (A, C, D):
// - R will be the provider for its first dependent
// - A change to R will require that its second dependent be replaced
// - A change to R will not require that its third dependent be replaced
//
// In addition, K will have a requires-replacement property that depends on both F and G, and
// L will have a normal property that depends on both F and G.
//
// With that in mind, the following resources should require replacement: A, B, C, E, F, and K
p := &TestPlan{}
urns, old, program := generateComplexTestDependencyGraph(t, p)
names := complexTestDependencyGraphNames
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{
ReplaceKeys: []resource.PropertyKey{"A"},
DeleteBeforeReplace: true,
}, nil
}
return plugin.DiffResult{}, nil
},
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil
}
return plugin.DiffResult{}, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
SkipPreview: true,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
replaced := make(map[resource.URN]bool)
for _, entry := range entries {
if entry.Step.Op() == deploy.OpReplace {
replaced[entry.Step.URN()] = true
}
}
assert.Equal(t, map[resource.URN]bool{
pickURN(t, urns, names, "A"): true,
pickURN(t, urns, names, "B"): true,
pickURN(t, urns, names, "C"): true,
pickURN(t, urns, names, "E"): true,
pickURN(t, urns, names, "F"): true,
pickURN(t, urns, names, "K"): true,
}, replaced)
return res
},
}}
p.Run(t, old)
}
func TestPropertyDependenciesAdapter(t *testing.T) {
// Ensure that the eval source properly shims in property dependencies if none were reported (and does not if
// any were reported).
type propertyDependencies map[resource.PropertyKey][]resource.URN
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
const resType = "pkgA:m:typA"
var urnA, urnB, urnC, urnD resource.URN
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
register := func(name string, inputs resource.PropertyMap, inputDeps propertyDependencies,
dependencies []resource.URN) resource.URN {
urn, _, _, err := monitor.RegisterResource(resType, name, true, deploytest.ResourceOptions{
Inputs: inputs,
Dependencies: dependencies,
PropertyDeps: inputDeps,
})
assert.NoError(t, err)
return urn
}
urnA = register("A", nil, nil, nil)
urnB = register("B", nil, nil, nil)
urnC = register("C", resource.PropertyMap{
"A": resource.NewStringProperty("foo"),
"B": resource.NewStringProperty("bar"),
}, nil, []resource.URN{urnA, urnB})
urnD = register("D", resource.PropertyMap{
"A": resource.NewStringProperty("foo"),
"B": resource.NewStringProperty("bar"),
}, propertyDependencies{
"A": []resource.URN{urnB},
"B": []resource.URN{urnA, urnC},
}, []resource.URN{urnA, urnB, urnC})
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{Op: Update}},
}
snap := p.Run(t, nil)
for _, res := range snap.Resources {
switch res.URN {
case urnA, urnB:
assert.Empty(t, res.Dependencies)
assert.Empty(t, res.PropertyDependencies)
case urnC:
assert.Equal(t, []resource.URN{urnA, urnB}, res.Dependencies)
assert.EqualValues(t, propertyDependencies{
"A": res.Dependencies,
"B": res.Dependencies,
}, res.PropertyDependencies)
case urnD:
assert.Equal(t, []resource.URN{urnA, urnB, urnC}, res.Dependencies)
assert.EqualValues(t, propertyDependencies{
"A": []resource.URN{urnB},
"B": []resource.URN{urnA, urnC},
}, res.PropertyDependencies)
}
}
}
func TestExplicitDeleteBeforeReplace(t *testing.T) {
p := &TestPlan{}
dbrDiff := false
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{
ReplaceKeys: []resource.PropertyKey{"A"},
DeleteBeforeReplace: dbrDiff,
}, nil
}
return plugin.DiffResult{}, nil
},
}, nil
}),
}
const resType = "pkgA:index:typ"
inputsA := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
dbrValue, dbrA := true, (*bool)(nil)
inputsB := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
var provURN, urnA, urnB resource.URN
var provID resource.ID
var err error
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
provURN, provID, _, err = monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true)
assert.NoError(t, err)
if provID == "" {
provID = providers.UnknownID
}
provRef, err := providers.NewReference(provURN, provID)
assert.NoError(t, err)
provA := provRef.String()
urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
Provider: provA,
Inputs: inputsA,
DeleteBeforeReplace: dbrA,
})
assert.NoError(t, err)
inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}}
urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
Provider: provA,
Inputs: inputsB,
Dependencies: []resource.URN{urnA},
PropertyDeps: inputDepsB,
})
assert.NoError(t, err)
return nil
})
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Steps = []TestStep{{Op: Update}}
snap := p.Run(t, nil)
// Change the value of resA.A. Only resA should be replaced, and the replacement should be create-before-delete.
inputsA["A"] = resource.NewStringProperty("bar")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpCreateReplacement, URN: urnA},
{Op: deploy.OpReplace, URN: urnA},
{Op: deploy.OpSame, URN: urnB},
{Op: deploy.OpDeleteReplaced, URN: urnA},
}, SuccessfulSteps(entries))
return res
},
}}
snap = p.Run(t, snap)
// Change the registration of resA such that it requires delete-before-replace and change the value of resA.A. Both
// resA and resB should be replaced, and the replacements should be delete-before-replace.
dbrA, inputsA["A"] = &dbrValue, resource.NewStringProperty("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpDeleteReplaced, URN: urnB},
{Op: deploy.OpDeleteReplaced, URN: urnA},
{Op: deploy.OpReplace, URN: urnA},
{Op: deploy.OpCreateReplacement, URN: urnA},
{Op: deploy.OpReplace, URN: urnB},
{Op: deploy.OpCreateReplacement, URN: urnB},
}, SuccessfulSteps(entries))
return res
},
}}
snap = p.Run(t, snap)
// Change the value of resB.A. Only resB should be replaced, and the replacement should be create-before-delete.
inputsB["A"] = resource.NewStringProperty("qux")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpSame, URN: urnA},
{Op: deploy.OpCreateReplacement, URN: urnB},
{Op: deploy.OpReplace, URN: urnB},
{Op: deploy.OpDeleteReplaced, URN: urnB},
}, SuccessfulSteps(entries))
return res
},
}}
snap = p.Run(t, snap)
// Change the registration of resA such that it no longer requires delete-before-replace and change the value of
// resA.A. Only resA should be replaced, and the replacement should be create-before-delete.
dbrA, inputsA["A"] = nil, resource.NewStringProperty("zam")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpCreateReplacement, URN: urnA},
{Op: deploy.OpReplace, URN: urnA},
{Op: deploy.OpSame, URN: urnB},
{Op: deploy.OpDeleteReplaced, URN: urnA},
}, SuccessfulSteps(entries))
return res
},
}}
snap = p.Run(t, snap)
// Change the diff of resA such that it requires delete-before-replace and change the value of resA.A. Both
// resA and resB should be replaced, and the replacements should be delete-before-replace.
dbrDiff, inputsA["A"] = true, resource.NewStringProperty("foo")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpDeleteReplaced, URN: urnB},
{Op: deploy.OpDeleteReplaced, URN: urnA},
{Op: deploy.OpReplace, URN: urnA},
{Op: deploy.OpCreateReplacement, URN: urnA},
{Op: deploy.OpReplace, URN: urnB},
{Op: deploy.OpCreateReplacement, URN: urnB},
}, SuccessfulSteps(entries))
return res
},
}}
snap = p.Run(t, snap)
// Change the registration of resA such that it disables delete-before-replace and change the value of
// resA.A. Only resA should be replaced, and the replacement should be create-before-delete.
dbrA, dbrValue, inputsA["A"] = &dbrValue, false, resource.NewStringProperty("bar")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpCreateReplacement, URN: urnA},
{Op: deploy.OpReplace, URN: urnA},
{Op: deploy.OpSame, URN: urnB},
{Op: deploy.OpDeleteReplaced, URN: urnA},
}, SuccessfulSteps(entries))
return res
},
}}
p.Run(t, snap)
}
func TestSingleResourceIgnoreChanges(t *testing.T) {
var expectedIgnoreChanges []string
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
assert.Equal(t, expectedIgnoreChanges, ignoreChanges)
return plugin.DiffResult{}, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
assert.Equal(t, expectedIgnoreChanges, ignoreChanges)
return resource.PropertyMap{}, resource.StatusOK, nil
},
}, nil
}),
}
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, ignoreChanges []string,
allowedOps []deploy.StepOp) *deploy.Snapshot {
expectedIgnoreChanges = ignoreChanges
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: props,
IgnoreChanges: ignoreChanges,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, event := range events {
if event.Type == ResourcePreEvent {
payload := event.Payload().(ResourcePreEventPayload)
assert.Subset(t, allowedOps, []deploy.StepOp{payload.Metadata.Op})
}
}
return res
},
},
},
}
return p.Run(t, snap)
}
snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 1,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a", "b.c"}, []deploy.StepOp{deploy.OpCreate})
// Ensure that a change to an ignored property results in an OpSame
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "bar",
},
}), []string{"a", "b.c"}, []deploy.StepOp{deploy.OpSame})
// Ensure that a change to an un-ignored property results in an OpUpdate
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 3,
"b": map[string]interface{}{
"c": "qux",
},
}), nil, []deploy.StepOp{deploy.OpUpdate})
// Ensure that a removing an ignored property results in an OpSame
snap = updateProgramWithProps(snap, resource.PropertyMap{}, []string{"a", "b"}, []deploy.StepOp{deploy.OpSame})
// Ensure that a removing an un-ignored property results in an OpUpdate
snap = updateProgramWithProps(snap, resource.PropertyMap{}, nil, []deploy.StepOp{deploy.OpUpdate})
// Ensure that adding an ignored property results in an OpSame
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 4,
"b": map[string]interface{}{
"c": "zed",
},
}), []string{"a", "b"}, []deploy.StepOp{deploy.OpSame})
// Ensure that adding an un-ignored property results in an OpUpdate
_ = updateProgramWithProps(snap, resource.PropertyMap{
"c": resource.NewNumberProperty(4),
}, []string{"a", "b"}, []deploy.StepOp{deploy.OpUpdate})
}
// TestDefaultProviderDiff tests that the engine can gracefully recover whenever a resource's default provider changes
// and there is no diff in the provider's inputs.
func TestDefaultProviderDiff(t *testing.T) {
const resName, resBName = "resA", "resB"
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("0.17.10"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
deploytest.NewProviderLoader("pkgA", semver.MustParse("0.17.11"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
deploytest.NewProviderLoader("pkgA", semver.MustParse("0.17.12"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
runProgram := func(base *deploy.Snapshot, versionA, versionB string, expectedStep deploy.StepOp) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", resName, true, deploytest.ResourceOptions{
Version: versionA,
})
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", resBName, true, deploytest.ResourceOptions{
Version: versionB,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, entry := range entries {
if entry.Kind != JournalEntrySuccess {
continue
}
switch entry.Step.URN().Name().String() {
case resName, resBName:
assert.Equal(t, expectedStep, entry.Step.Op())
}
}
return res
},
},
},
}
return p.Run(t, base)
}
// This test simulates the upgrade scenario of old-style default providers to new-style versioned default providers.
//
// The first update creates a stack using a language host that does not report a version to the engine. As a result,
// the engine makes up a default provider for "pkgA" and calls it "default". It then creates the two resources that
// we are creating and associates them with the default provider.
snap := runProgram(nil, "", "", deploy.OpCreate)
for _, res := range snap.Resources {
switch {
case providers.IsDefaultProvider(res.URN):
assert.Equal(t, "default", res.URN.Name().String())
case res.URN.Name().String() == resName || res.URN.Name().String() == resBName:
provRef, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
assert.Equal(t, "default", provRef.URN().Name().String())
}
}
// The second update switches to a language host that does report a version to the engine. As a result, the engine
// uses this version to make a new provider, with a different URN, and uses that provider to operate on resA and
// resB.
//
// Despite switching out the provider, the engine should still generate a Same step for resA. It is vital that the
// engine gracefully react to changes in the default provider in this manner. See pulumi/pulumi#2753 for what
// happens when it doesn't.
snap = runProgram(snap, "0.17.10", "0.17.10", deploy.OpSame)
for _, res := range snap.Resources {
switch {
case providers.IsDefaultProvider(res.URN):
assert.Equal(t, "default_0_17_10", res.URN.Name().String())
case res.URN.Name().String() == resName || res.URN.Name().String() == resBName:
provRef, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
assert.Equal(t, "default_0_17_10", provRef.URN().Name().String())
}
}
// The third update changes the version that the language host reports to the engine. This simulates a scenario in
// which a user updates their SDK to a new version of a provider package. In order to simulate side-by-side
// packages with different versions, this update requests distinct package versions for resA and resB.
snap = runProgram(snap, "0.17.11", "0.17.12", deploy.OpSame)
for _, res := range snap.Resources {
switch {
case providers.IsDefaultProvider(res.URN):
assert.True(t, res.URN.Name().String() == "default_0_17_11" || res.URN.Name().String() == "default_0_17_12")
case res.URN.Name().String() == resName:
provRef, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
assert.Equal(t, "default_0_17_11", provRef.URN().Name().String())
case res.URN.Name().String() == resBName:
provRef, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
assert.Equal(t, "default_0_17_12", provRef.URN().Name().String())
}
}
}
// TestDefaultProviderDiffReplacement tests that, when replacing a default provider for a resource, the engine will
// replace the resource if DiffConfig on the new provider returns a diff for the provider's new state.
func TestDefaultProviderDiffReplacement(t *testing.T) {
const resName, resBName = "resA", "resB"
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("0.17.10"), func() (plugin.Provider, error) {
return &deploytest.Provider{
// This implementation of DiffConfig always requests replacement.
DiffConfigF: func(_ resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
keys := []resource.PropertyKey{}
for k := range news {
keys = append(keys, k)
}
return plugin.DiffResult{
Changes: plugin.DiffSome,
ReplaceKeys: keys,
}, nil
},
}, nil
}),
deploytest.NewProviderLoader("pkgA", semver.MustParse("0.17.11"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
runProgram := func(base *deploy.Snapshot, versionA, versionB string, expectedSteps ...deploy.StepOp) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", resName, true, deploytest.ResourceOptions{
Version: versionA,
})
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", resBName, true, deploytest.ResourceOptions{
Version: versionB,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, entry := range entries {
if entry.Kind != JournalEntrySuccess {
continue
}
switch entry.Step.URN().Name().String() {
case resName:
assert.Subset(t, expectedSteps, []deploy.StepOp{entry.Step.Op()})
case resBName:
assert.Subset(t,
[]deploy.StepOp{deploy.OpCreate, deploy.OpSame}, []deploy.StepOp{entry.Step.Op()})
}
}
return res
},
},
},
}
return p.Run(t, base)
}
// This test simulates the upgrade scenario of default providers, except that the requested upgrade results in the
// provider getting replaced. Because of this, the engine should decide to replace resA. It should not decide to
// replace resB, as its change does not require replacement.
snap := runProgram(nil, "", "", deploy.OpCreate)
for _, res := range snap.Resources {
switch {
case providers.IsDefaultProvider(res.URN):
assert.Equal(t, "default", res.URN.Name().String())
case res.URN.Name().String() == resName || res.URN.Name().String() == resBName:
provRef, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
assert.Equal(t, "default", provRef.URN().Name().String())
}
}
// Upon update, now that the language host is sending a version, DiffConfig reports that there's a diff between the
// old and new provider and so we must replace resA.
snap = runProgram(snap, "0.17.10", "0.17.11", deploy.OpCreateReplacement, deploy.OpReplace, deploy.OpDeleteReplaced)
for _, res := range snap.Resources {
switch {
case providers.IsDefaultProvider(res.URN):
assert.True(t, res.URN.Name().String() == "default_0_17_10" || res.URN.Name().String() == "default_0_17_11")
case res.URN.Name().String() == resName:
provRef, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
assert.Equal(t, "default_0_17_10", provRef.URN().Name().String())
case res.URN.Name().String() == resBName:
provRef, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
assert.Equal(t, "default_0_17_11", provRef.URN().Name().String())
}
}
}
// Resource is an abstract representation of a resource graph
type Resource struct {
t tokens.Type
name string
children []Resource
props resource.PropertyMap
aliases []resource.URN
dependencies []resource.URN
parent resource.URN
deleteBeforeReplace bool
}
func registerResources(t *testing.T, monitor *deploytest.ResourceMonitor, resources []Resource) error {
for _, r := range resources {
_, _, _, err := monitor.RegisterResource(r.t, r.name, true, deploytest.ResourceOptions{
Parent: r.parent,
Dependencies: r.dependencies,
Inputs: r.props,
DeleteBeforeReplace: &r.deleteBeforeReplace,
Aliases: r.aliases,
})
if err != nil {
return err
}
err = registerResources(t, monitor, r.children)
if err != nil {
return err
}
}
return nil
}
func TestAliases(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
// The `forcesReplacement` key forces replacement and all other keys can update in place
DiffF: func(res resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
replaceKeys := []resource.PropertyKey{}
old, hasOld := olds["forcesReplacement"]
new, hasNew := news["forcesReplacement"]
if hasOld && !hasNew || hasNew && !hasOld || hasOld && hasNew && old.Diff(new) != nil {
replaceKeys = append(replaceKeys, "forcesReplacement")
}
return plugin.DiffResult{ReplaceKeys: replaceKeys}, nil
},
}, nil
}),
}
updateProgramWithResource := func(
snap *deploy.Snapshot, resources []Resource, allowedOps []deploy.StepOp) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
err := registerResources(t, monitor, resources)
return err
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, event := range events {
if event.Type == ResourcePreEvent {
payload := event.Payload().(ResourcePreEventPayload)
assert.Subset(t, allowedOps, []deploy.StepOp{payload.Metadata.Op})
}
}
for _, entry := range entries {
if entry.Step.Type() == "pulumi:providers:pkgA" {
continue
}
switch entry.Kind {
case JournalEntrySuccess:
assert.Subset(t, allowedOps, []deploy.StepOp{entry.Step.Op()})
case JournalEntryFailure:
assert.Fail(t, "unexpected failure in journal")
case JournalEntryBegin:
case JournalEntryOutputs:
}
}
return res
},
},
},
}
return p.Run(t, snap)
}
snap := updateProgramWithResource(nil, []Resource{{
t: "pkgA:index:t1",
name: "n1",
}}, []deploy.StepOp{deploy.OpCreate})
// Ensure that rename produces Same
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t1",
name: "n2",
aliases: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n1"},
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that rename produces Same with multiple aliases
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t1",
name: "n3",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
"urn:pulumi:test::test::pkgA:index:t1::n2",
},
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that rename produces Same with multiple aliases (reversed)
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t1",
name: "n3",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n2",
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that aliasing back to original name is okay
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t1",
name: "n1",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n3",
"urn:pulumi:test::test::pkgA:index:t1::n2",
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that removing aliases is okay (once old names are gone from all snapshots)
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t1",
name: "n1",
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that changing the type works
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t2",
name: "n1",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that changing the type again works
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:othermod:t3",
name: "n1",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
"urn:pulumi:test::test::pkgA:index:t2::n1",
},
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that order of aliases doesn't matter
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:othermod:t3",
name: "n1",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
"urn:pulumi:test::test::pkgA:othermod:t3::n1",
"urn:pulumi:test::test::pkgA:index:t2::n1",
},
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that removing aliases is okay (once old names are gone from all snapshots)
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:othermod:t3",
name: "n1",
}}, []deploy.StepOp{deploy.OpSame})
// Ensure that changing everything (including props) leads to update not delete and re-create
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t4",
name: "n2",
props: resource.PropertyMap{
resource.PropertyKey("x"): resource.NewNumberProperty(42),
},
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:othermod:t3::n1",
},
}}, []deploy.StepOp{deploy.OpUpdate})
// Ensure that changing everything again (including props) leads to update not delete and re-create
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t5",
name: "n3",
props: resource.PropertyMap{
resource.PropertyKey("x"): resource.NewNumberProperty(1000),
},
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t4::n2",
},
}}, []deploy.StepOp{deploy.OpUpdate})
// Ensure that changing a forceNew property while also changing type and name leads to replacement not delete+create
snap = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t6",
name: "n4",
props: resource.PropertyMap{
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1000),
},
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t5::n3",
},
}}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that changing a forceNew property and deleteBeforeReplace while also changing type and name leads to
// replacement not delete+create
_ = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t7",
name: "n5",
props: resource.PropertyMap{
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(999),
},
deleteBeforeReplace: true,
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t6::n4",
},
}}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Start again - this time with two resources with depends on relationship
snap = updateProgramWithResource(nil, []Resource{{
t: "pkgA:index:t1",
name: "n1",
props: resource.PropertyMap{
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1),
},
deleteBeforeReplace: true,
}, {
t: "pkgA:index:t2",
name: "n2",
dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n1"},
}}, []deploy.StepOp{deploy.OpCreate})
_ = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t1-new",
name: "n1-new",
props: resource.PropertyMap{
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2),
},
deleteBeforeReplace: true,
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}, {
t: "pkgA:index:t2-new",
name: "n2-new",
dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1-new::n1-new"},
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t2::n2",
},
}}, []deploy.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Start again - this time with two resources with parent relationship
snap = updateProgramWithResource(nil, []Resource{{
t: "pkgA:index:t1",
name: "n1",
props: resource.PropertyMap{
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1),
},
deleteBeforeReplace: true,
}, {
t: "pkgA:index:t2",
name: "n2",
parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1::n1"),
}}, []deploy.StepOp{deploy.OpCreate})
_ = updateProgramWithResource(snap, []Resource{{
t: "pkgA:index:t1-new",
name: "n1-new",
props: resource.PropertyMap{
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2),
},
deleteBeforeReplace: true,
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}, {
t: "pkgA:index:t2-new",
name: "n2-new",
parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new::n1-new"),
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n2",
},
}}, []deploy.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// ensure failure when different resources use duplicate aliases
snap = updateProgramWithResource(nil, []Resource{{
t: "pkgA:index:t1",
name: "n1",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}, {
t: "pkgA:index:t2",
name: "n2",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}}, []deploy.StepOp{deploy.OpCreate})
err := snap.NormalizeURNReferences()
assert.Equal(t, err.Error(),
"Two resources ('urn:pulumi:test::test::pkgA:index:t1::n1'"+
" and 'urn:pulumi:test::test::pkgA:index:t2::n2') aliased to the same: 'urn:pulumi:test::test::pkgA:index:t1::n1'")
// ensure different resources can use different aliases
snap = updateProgramWithResource(nil, []Resource{{
t: "pkgA:index:t1",
name: "n1",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n1",
},
}, {
t: "pkgA:index:t2",
name: "n2",
aliases: []resource.URN{
"urn:pulumi:test::test::pkgA:index:t1::n2",
},
}}, []deploy.StepOp{deploy.OpCreate})
err = snap.NormalizeURNReferences()
assert.Nil(t, err)
}
func TestPersistentDiff(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
return plugin.DiffResult{Changes: plugin.DiffSome}, nil
},
}, nil
}),
}
inputs := resource.PropertyMap{}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: inputs,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Run the initial update.
project := p.GetProject()
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
assert.Nil(t, res)
// First, make no change to the inputs and run a preview. We should see an update to the resource due to
// provider diffing.
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
events []Event, res result.Result) result.Result {
found := false
for _, e := range events {
if e.Type == ResourcePreEvent {
p := e.Payload().(ResourcePreEventPayload).Metadata
if p.URN == resURN {
assert.Equal(t, deploy.OpUpdate, p.Op)
found = true
}
}
}
assert.True(t, found)
return res
})
assert.Nil(t, res)
// Next, enable legacy diff behavior. We should see no changes to the resource.
p.Options.UseLegacyDiff = true
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
events []Event, res result.Result) result.Result {
found := false
for _, e := range events {
if e.Type == ResourcePreEvent {
p := e.Payload().(ResourcePreEventPayload).Metadata
if p.URN == resURN {
assert.Equal(t, deploy.OpSame, p.Op)
found = true
}
}
}
assert.True(t, found)
return res
})
assert.Nil(t, res)
}
func TestDetailedDiffReplace(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
return plugin.DiffResult{
Changes: plugin.DiffSome,
DetailedDiff: map[string]plugin.PropertyDiff{
"prop": {Kind: plugin.DiffAddReplace},
},
}, nil
},
}, nil
}),
}
inputs := resource.PropertyMap{}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: inputs,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Run the initial update.
project := p.GetProject()
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
assert.Nil(t, res)
// First, make no change to the inputs and run a preview. We should see an update to the resource due to
// provider diffing.
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
events []Event, res result.Result) result.Result {
found := false
for _, e := range events {
if e.Type == ResourcePreEvent {
p := e.Payload().(ResourcePreEventPayload).Metadata
if p.URN == resURN && p.Op == deploy.OpReplace {
found = true
}
}
}
assert.True(t, found)
return res
})
assert.Nil(t, res)
}
func TestImport(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if olds["foo"].DeepEquals(news["foo"]) {
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
diffKind := plugin.DiffUpdate
if news["foo"].IsString() && news["foo"].StringValue() == "replace" {
diffKind = plugin.DiffUpdateReplace
}
return plugin.DiffResult{
Changes: plugin.DiffSome,
DetailedDiff: map[string]plugin.PropertyDiff{
"foo": {Kind: diffKind},
},
}, nil
},
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{
Inputs: resource.PropertyMap{
"foo": resource.NewStringProperty("bar"),
},
Outputs: resource.PropertyMap{
"foo": resource.NewStringProperty("bar"),
},
}, resource.StatusOK, nil
},
}, nil
}),
}
readID, importID, inputs := resource.ID(""), resource.ID("id"), resource.PropertyMap{}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
var err error
if readID != "" {
_, _, err = monitor.ReadResource("pkgA:m:typA", "resA", readID, "", resource.PropertyMap{}, "", "")
} else {
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: inputs,
ImportID: importID,
})
}
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// 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)
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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN:
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
case resURN:
assert.Equal(t, deploy.OpImport, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN, resURN:
assert.Equal(t, deploy.OpSame, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
// 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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN:
assert.Equal(t, deploy.OpSame, entry.Step.Op())
case resURN:
assert.Equal(t, deploy.OpUpdate, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
// 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)
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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN, resURN:
assert.Equal(t, deploy.OpDelete, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
// 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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN, resURN:
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
assert.Len(t, snap.Resources, 2)
// Set the import ID to the same ID as the existing resource and run an update. This should produce no changes.
for _, r := range snap.Resources {
if r.URN == resURN {
importID = r.ID
}
}
snap, res = TestOp(Update).Run(project, p.GetTarget(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 {
case provURN, resURN:
assert.Equal(t, deploy.OpSame, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
// 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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN:
assert.Equal(t, deploy.OpSame, entry.Step.Op())
case resURN:
switch entry.Step.Op() {
case deploy.OpReplace, deploy.OpImportReplacement:
assert.Equal(t, importID, entry.Step.New().ID)
case deploy.OpDeleteReplaced:
assert.NotEqual(t, importID, entry.Step.Old().ID)
}
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
// 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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN:
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
case resURN:
assert.Equal(t, deploy.OpRead, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
assert.Len(t, snap.Resources, 2)
// 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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN:
assert.Equal(t, deploy.OpSame, entry.Step.Op())
case resURN:
switch entry.Step.Op() {
case deploy.OpReplace, deploy.OpImportReplacement:
assert.Equal(t, importID, entry.Step.New().ID)
case deploy.OpDiscardReplaced:
assert.Equal(t, importID, entry.Step.Old().ID)
}
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
}
// TestImportWithDifferingImportIdentifierFormat tests importing a resource that has a different format of identifier
// for the import input than for the ID property, ensuring that a second update does not result in a replace.
func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if olds["foo"].DeepEquals(news["foo"]) {
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
return plugin.DiffResult{
Changes: plugin.DiffSome,
DetailedDiff: map[string]plugin.PropertyDiff{
"foo": {Kind: plugin.DiffUpdate},
},
}, nil
},
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{
// This ID is deliberately not the same as the ID used to import.
ID: "id",
Inputs: resource.PropertyMap{
"foo": resource.NewStringProperty("bar"),
},
Outputs: resource.PropertyMap{
"foo": resource.NewStringProperty("bar"),
},
}, resource.StatusOK, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: resource.PropertyMap{
"foo": resource.NewStringProperty("bar"),
},
// The import ID is deliberately not the same as the ID returned from Read.
ImportID: resource.ID("import-id"),
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// 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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN:
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
case resURN:
assert.Equal(t, deploy.OpImport, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
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,
func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, res result.Result) result.Result {
for _, entry := range entries {
switch urn := entry.Step.URN(); urn {
case provURN, resURN:
assert.Equal(t, deploy.OpSame, entry.Step.Op())
default:
t.Fatalf("unexpected resource %v", urn)
}
}
return res
})
assert.Nil(t, res)
}
func TestCustomTimeouts(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
CustomTimeouts: &resource.CustomTimeouts{
Create: 60, Delete: 60, Update: 240,
},
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
p.Steps = []TestStep{{Op: Update}}
snap := p.Run(t, nil)
assert.Len(t, snap.Resources, 2)
assert.Equal(t, string(snap.Resources[0].URN.Name()), "default")
assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
assert.NotNil(t, snap.Resources[1].CustomTimeouts)
assert.Equal(t, snap.Resources[1].CustomTimeouts.Create, float64(60))
assert.Equal(t, snap.Resources[1].CustomTimeouts.Update, float64(240))
assert.Equal(t, snap.Resources[1].CustomTimeouts.Delete, float64(60))
}
func TestProviderDiffMissingOldOutputs(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// Always require replacement if any diff exists.
if !olds.DeepEquals(news) {
keys := []resource.PropertyKey{}
for k := range news {
keys = append(keys, k)
}
return plugin.DiffResult{Changes: plugin.DiffSome, ReplaceKeys: keys}, nil
}
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Config: config.Map{
config.MustMakeKey("pkgA", "foo"): config.NewValue("bar"),
},
}
// Build a basic lifecycle.
steps := MakeBasicLifecycleSteps(t, 2)
// Run the lifecycle through its initial update and refresh.
p.Steps = steps[:2]
snap := p.Run(t, nil)
// Delete the old provider outputs (if any) from the checkpoint, then run the no-op update.
providerURN := p.NewProviderURN("pkgA", "default", "")
for _, r := range snap.Resources {
if r.URN == providerURN {
r.Outputs = nil
}
}
p.Steps = steps[2:3]
snap = p.Run(t, snap)
// Change the config, delete the old provider outputs, and run an update. We expect everything to require
// replacement.
p.Config[config.MustMakeKey("pkgA", "foo")] = config.NewValue("baz")
for _, r := range snap.Resources {
if r.URN == providerURN {
r.Outputs = nil
}
}
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
resURN := p.NewURN("pkgA:m:typA", "resA", "")
// Look for replace steps on the provider and the resource.
replacedProvider, replacedResource := false, false
for _, entry := range entries {
if entry.Kind != JournalEntrySuccess || entry.Step.Op() != deploy.OpDeleteReplaced {
continue
}
switch urn := entry.Step.URN(); urn {
case providerURN:
replacedProvider = true
case resURN:
replacedResource = true
default:
t.Fatalf("unexpected resource %v", urn)
}
}
assert.True(t, replacedProvider)
assert.True(t, replacedResource)
return res
},
}}
p.Run(t, snap)
}
func TestRefreshStepWillPersistUpdatedIDs(t *testing.T) {
p := &TestPlan{}
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
idBefore := resource.ID("myid")
idAfter := resource.ID("mynewid")
outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{ID: idAfter, Outputs: outputs, Inputs: resource.PropertyMap{}}, resource.StatusOK, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", false)
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Options.Host = host
old := &deploy.Snapshot{
Resources: []*resource.State{
{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: idBefore,
Inputs: resource.PropertyMap{},
Outputs: outputs,
InitErrors: []string{"Resource failed to initialize"},
},
},
}
p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}
snap := p.Run(t, old)
for _, resource := range snap.Resources {
switch urn := resource.URN; urn {
case provURN:
// break
case resURN:
assert.Empty(t, resource.InitErrors)
assert.Equal(t, idAfter, resource.ID)
default:
t.Fatalf("unexpected resource %v", urn)
}
}
}
func TestMissingRead(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(_ resource.URN, _ resource.ID, _, _ resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{}, resource.StatusOK, nil
},
}, nil
}),
}
// Our program reads a resource and exits.
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "resA-some-id", "", resource.PropertyMap{}, "", "")
assert.Error(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{Op: Update, ExpectFailure: true}},
}
p.Run(t, nil)
}
func TestImportUpdatedID(t *testing.T) {
p := &TestPlan{}
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
importID := resource.ID("myID")
actualID := resource.ID("myNewID")
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{
ID: actualID,
Outputs: resource.PropertyMap{},
Inputs: resource.PropertyMap{},
}, resource.StatusOK, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, id, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
ImportID: importID,
})
assert.NoError(t, err)
assert.Equal(t, actualID, id)
return nil
})
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}
snap := p.Run(t, nil)
for _, resource := range snap.Resources {
switch urn := resource.URN; urn {
case provURN:
// break
case resURN:
assert.Equal(t, actualID, resource.ID)
default:
t.Fatalf("unexpected resource %v", urn)
}
}
}
func TestDestroyTarget(t *testing.T) {
// Try refreshing a stack with combinations of the above resources as target to destroy.
subsets := combinations.All(complexTestDependencyGraphNames)
for _, subset := range subsets {
// limit to up to 3 resources to destroy. This keeps the test running time under
// control as it only generates a few hundred combinations instead of several thousand.
if len(subset) <= 3 {
destroySpecificTargets(t, subset, true, /*targetDependents*/
func(urns []resource.URN, deleted map[resource.URN]bool) {})
}
}
destroySpecificTargets(
t, []string{"A"}, true, /*targetDependents*/
func(urns []resource.URN, deleted map[resource.URN]bool) {
// when deleting 'A' we expect A, B, C, E, F, and K to be deleted
names := complexTestDependencyGraphNames
assert.Equal(t, map[resource.URN]bool{
pickURN(t, urns, names, "A"): true,
pickURN(t, urns, names, "B"): true,
pickURN(t, urns, names, "C"): true,
pickURN(t, urns, names, "E"): true,
pickURN(t, urns, names, "F"): true,
pickURN(t, urns, names, "K"): true,
}, deleted)
})
destroySpecificTargets(
t, []string{"A"}, false, /*targetDependents*/
func(urns []resource.URN, deleted map[resource.URN]bool) {})
}
func destroySpecificTargets(
t *testing.T, targets []string, targetDependents bool,
validate func(urns []resource.URN, deleted map[resource.URN]bool)) {
// A
// _________|_________
// B C D
// ___|___ ___|___
// E F G H I J
// |__|
// K L
p := &TestPlan{}
urns, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{
ReplaceKeys: []resource.PropertyKey{"A"},
DeleteBeforeReplace: true,
}, nil
}
return plugin.DiffResult{}, nil
},
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil
}
return plugin.DiffResult{}, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Options.TargetDependents = targetDependents
destroyTargets := []resource.URN{}
for _, target := range targets {
destroyTargets = append(destroyTargets, pickURN(t, urns, complexTestDependencyGraphNames, target))
}
p.Options.DestroyTargets = destroyTargets
t.Logf("Destroying targets: %v", destroyTargets)
// If we're not forcing the targets to be destroyed, then expect to get a failure here as
// we'll have downstream resources to delete that weren't specified explicitly.
p.Steps = []TestStep{{
Op: Destroy,
ExpectFailure: !targetDependents,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
deleted := make(map[resource.URN]bool)
for _, entry := range entries {
assert.Equal(t, deploy.OpDelete, entry.Step.Op())
deleted[entry.Step.URN()] = true
}
for _, target := range p.Options.DestroyTargets {
assert.Contains(t, deleted, target)
}
validate(urns, deleted)
return res
},
}}
p.Run(t, old)
}
func TestUpdateTarget(t *testing.T) {
// Try refreshing a stack with combinations of the above resources as target to destroy.
subsets := combinations.All(complexTestDependencyGraphNames)
for _, subset := range subsets {
// limit to up to 3 resources to destroy. This keeps the test running time under
// control as it only generates a few hundred combinations instead of several thousand.
if len(subset) <= 3 {
updateSpecificTargets(t, subset)
}
}
updateSpecificTargets(t, []string{"A"})
// Also update a target that doesn't exist to make sure we don't crash or otherwise go off the rails.
updateInvalidTarget(t)
}
func updateSpecificTargets(t *testing.T, targets []string) {
// A
// _________|_________
// B C D
// ___|___ ___|___
// E F G H I J
// |__|
// K L
p := &TestPlan{}
urns, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// all resources will change.
return plugin.DiffResult{
Changes: plugin.DiffSome,
}, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
outputs := olds.Copy()
outputs["output_prop"] = resource.NewPropertyValue(42)
return outputs, resource.StatusOK, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
updateTargets := []resource.URN{}
for _, target := range targets {
updateTargets = append(updateTargets,
pickURN(t, urns, complexTestDependencyGraphNames, target))
}
p.Options.UpdateTargets = updateTargets
t.Logf("Updating targets: %v", updateTargets)
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
updated := make(map[resource.URN]bool)
sames := make(map[resource.URN]bool)
for _, entry := range entries {
if entry.Step.Op() == deploy.OpUpdate {
updated[entry.Step.URN()] = true
} else if entry.Step.Op() == deploy.OpSame {
sames[entry.Step.URN()] = true
} else {
assert.FailNowf(t, "", "Got a step that wasn't a same/update: %v", entry.Step.Op())
}
}
for _, target := range p.Options.UpdateTargets {
assert.Contains(t, updated, target)
}
for _, target := range p.Options.UpdateTargets {
assert.NotContains(t, sames, target)
}
return res
},
}}
p.Run(t, old)
}
func updateInvalidTarget(t *testing.T) {
p := &TestPlan{}
_, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// all resources will change.
return plugin.DiffResult{
Changes: plugin.DiffSome,
}, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
outputs := olds.Copy()
outputs["output_prop"] = resource.NewPropertyValue(42)
return outputs, resource.StatusOK, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Options.UpdateTargets = []resource.URN{"foo"}
t.Logf("Updating invalid targets: %v", p.Options.UpdateTargets)
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: true,
}}
p.Run(t, old)
}
func TestCreateDuringTargetedUpdate_CreateMentionedAsTarget(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
snap1 := p.Run(t, nil)
// Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
resA := p.NewURN("pkgA:m:typA", "resA", "")
resB := p.NewURN("pkgA:m:typA", "resB", "")
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA, resB}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
for _, entry := range entries {
if entry.Step.URN() == resA {
assert.Equal(t, deploy.OpSame, entry.Step.Op())
} else if entry.Step.URN() == resB {
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
}
}
return res
},
}}
p.Run(t, snap1)
}
func TestCreateDuringTargetedUpdate_UntargetedCreateNotReferenced(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
snap1 := p.Run(t, nil)
// Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
resA := p.NewURN("pkgA:m:typA", "resA", "")
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
for _, entry := range entries {
// everything should be a same op here.
assert.Equal(t, deploy.OpSame, entry.Step.Op())
}
return res
},
}}
p.Run(t, snap1)
}
func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByTarget(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
p.Run(t, nil)
resA := p.NewURN("pkgA:m:typA", "resA", "")
resB := p.NewURN("pkgA:m:typA", "resB", "")
// Now, create a resource resB. But reference it from A. This will cause a dependency we can't
// satisfy.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true,
deploytest.ResourceOptions{
Dependencies: []resource.URN{resB},
})
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: true,
}}
p.Run(t, nil)
}
func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByUntargetedCreate(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
snap1 := p.Run(t, nil)
resA := p.NewURN("pkgA:m:typA", "resA", "")
resB := p.NewURN("pkgA:m:typA", "resB", "")
// Now, create a resource resB. But reference it from A. This will cause a dependency we can't
// satisfy.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resC", true,
deploytest.ResourceOptions{
Dependencies: []resource.URN{resB},
})
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
for _, entry := range entries {
assert.Equal(t, deploy.OpSame, entry.Step.Op())
}
return res
},
}}
p.Run(t, snap1)
}
func TestDependencyChangeDBR(t *testing.T) {
p := &TestPlan{}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{
ReplaceKeys: []resource.PropertyKey{"A"},
DeleteBeforeReplace: true,
}, nil
}
if !olds["B"].DeepEquals(news["B"]) {
return plugin.DiffResult{
Changes: plugin.DiffSome,
}, nil
}
return plugin.DiffResult{}, nil
},
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
}, nil
}),
}
const resType = "pkgA:index:typ"
inputsA := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
inputsB := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
var urnA, urnB resource.URN
var err error
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
Inputs: inputsA,
})
assert.NoError(t, err)
inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}}
urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
Inputs: inputsB,
Dependencies: []resource.URN{urnA},
PropertyDeps: inputDepsB,
})
assert.NoError(t, err)
return nil
})
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Steps = []TestStep{{Op: Update}}
snap := p.Run(t, nil)
inputsA["A"] = resource.NewStringProperty("bar")
program = deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
Inputs: inputsB,
})
assert.NoError(t, err)
urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
Inputs: inputsA,
})
assert.NoError(t, err)
return nil
})
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Steps = []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
resBDeleted, resBSame := false, false
for _, entry := range entries {
if entry.Step.URN() == urnB {
switch entry.Step.Op() {
case deploy.OpDelete, deploy.OpDeleteReplaced:
resBDeleted = true
case deploy.OpSame:
resBSame = true
}
}
}
assert.True(t, resBSame)
assert.False(t, resBDeleted)
return res
},
},
}
p.Run(t, snap)
}
func TestReplaceSpecificTargets(t *testing.T) {
// A
// _________|_________
// B C D
// ___|___ ___|___
// E F G H I J
// |__|
// K L
p := &TestPlan{}
urns, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// No resources will change.
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
},
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
getURN := func(name string) resource.URN {
return pickURN(t, urns, complexTestDependencyGraphNames, name)
}
p.Options.ReplaceTargets = []resource.URN{
getURN("F"),
getURN("B"),
getURN("G"),
}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
replaced := make(map[resource.URN]bool)
sames := make(map[resource.URN]bool)
for _, entry := range entries {
if entry.Step.Op() == deploy.OpReplace {
replaced[entry.Step.URN()] = true
} else if entry.Step.Op() == deploy.OpSame {
sames[entry.Step.URN()] = true
}
}
for _, target := range p.Options.ReplaceTargets {
assert.Contains(t, replaced, target)
}
for _, target := range p.Options.ReplaceTargets {
assert.NotContains(t, sames, target)
}
return res
},
}}
p.Run(t, old)
}
func TestProviderPreview(t *testing.T) {
sawPreview := false
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
if preview {
sawPreview = true
}
assert.Equal(t, preview, news.ContainsUnknowns())
return "created-id", news, resource.StatusOK, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
if preview {
sawPreview = true
}
assert.Equal(t, preview, news.ContainsUnknowns())
return news, resource.StatusOK, nil
},
}, nil
}),
}
preview := true
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")})
if !preview {
computed = "alpha"
}
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "bar",
"baz": map[string]interface{}{
"a": 42,
"b": computed,
},
"qux": []interface{}{
computed,
24,
},
"zed": computed,
})
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: ins,
})
assert.NoError(t, err)
assert.True(t, state.DeepEquals(ins))
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
project := p.GetProject()
// Run a preview. The inputs should be propagated to the outputs by the provider during the create.
preview, sawPreview = true, false
_, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
assert.Nil(t, res)
assert.True(t, sawPreview)
// Run an update.
preview, sawPreview = false, false
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
assert.Nil(t, res)
assert.False(t, sawPreview)
// Run another preview. The inputs should be propagated to the outputs during the update.
preview, sawPreview = true, false
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, preview, p.BackendClient, nil)
assert.Nil(t, res)
assert.True(t, sawPreview)
}
type testResource struct {
pulumi.CustomResourceState
Foo pulumi.StringOutput `pulumi:"foo"`
}
type testResourceArgs struct {
Foo string `pulumi:"foo"`
Bar string `pulumi:"bar"`
Baz string `pulumi:"baz"`
Bang string `pulumi:"bang"`
}
type testResourceInputs struct {
Foo pulumi.StringInput
Bar pulumi.StringInput
Baz pulumi.StringInput
Bang pulumi.StringInput
}
func (*testResourceInputs) ElementType() reflect.Type {
return reflect.TypeOf((*testResourceArgs)(nil))
}
func TestSingleResourceDefaultProviderGolangLifecycle(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{
Project: info.Project,
Stack: info.Stack,
Parallel: info.Parallel,
DryRun: info.DryRun,
MonitorAddr: info.MonitorAddress,
})
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
var resA testResource
err := ctx.RegisterResource("pkgA:m:typA", "resA", &testResourceInputs{
Foo: pulumi.String("bar"),
}, &resA)
assert.NoError(t, err)
var resB testResource
err = ctx.RegisterResource("pkgA:m:typA", "resB", &testResourceInputs{
Baz: resA.Foo.ApplyT(func(v string) string {
return v + "bar"
}).(pulumi.StringOutput),
}, &resB)
assert.NoError(t, err)
return nil
})
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: MakeBasicLifecycleSteps(t, 4),
}
p.Run(t, nil)
}
// Inspired by transformations_test.go.
func TestSingleResourceDefaultProviderGolangTransformations(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
},
}, nil
}),
}
newResource := func(ctx *pulumi.Context, name string, opts ...pulumi.ResourceOption) error {
var res testResource
return ctx.RegisterResource("pkgA:m:typA", name, &testResourceInputs{
Foo: pulumi.String("bar"),
}, &res, opts...)
}
newComponent := func(ctx *pulumi.Context, name string, opts ...pulumi.ResourceOption) error {
var res testResource
err := ctx.RegisterComponentResource("pkgA:m:typA", name, &res, opts...)
if err != nil {
return err
}
var resChild testResource
return ctx.RegisterResource("pkgA:m:typA", name+"Child", &testResourceInputs{
Foo: pulumi.String("bar"),
}, &resChild, pulumi.Parent(&res))
}
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{
Project: info.Project,
Stack: info.Stack,
Parallel: info.Parallel,
DryRun: info.DryRun,
MonitorAddr: info.MonitorAddress,
})
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
// Scenario #1 - apply a transformation to a CustomResource
res1Transformation := func(args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
// TODO[pulumi/pulumi#3846] We should use a mergeOptions-style API here.
return &pulumi.ResourceTransformationResult{
Props: args.Props,
Opts: append(args.Opts, pulumi.AdditionalSecretOutputs([]string{"output"})),
}
}
assert.NoError(t, newResource(ctx, "res1",
pulumi.Transformations([]pulumi.ResourceTransformation{res1Transformation})))
// Scenario #2 - apply a transformation to a Component to transform its children
res2Transformation := func(args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
if args.Name == "res2Child" {
// TODO[pulumi/pulumi#3846] We should use a mergeOptions-style API here.
return &pulumi.ResourceTransformationResult{
Props: args.Props,
Opts: append(args.Opts, pulumi.AdditionalSecretOutputs([]string{"output", "output2"})),
}
}
return nil
}
assert.NoError(t, newComponent(ctx, "res2",
pulumi.Transformations([]pulumi.ResourceTransformation{res2Transformation})))
// Scenario #3 - apply a transformation to the Stack to transform all (future) resources in the stack
res3Transformation := func(args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
// Props might be nil.
var props *testResourceInputs
if args.Props == nil {
props = &testResourceInputs{}
} else {
props = args.Props.(*testResourceInputs)
}
props.Foo = pulumi.String("baz")
return &pulumi.ResourceTransformationResult{
Props: props,
Opts: args.Opts,
}
}
assert.NoError(t, ctx.RegisterStackTransformation(res3Transformation))
assert.NoError(t, newResource(ctx, "res3"))
// Scenario #4 - transformations are applied in order of decreasing specificity
// 1. (not in this example) Child transformation
// 2. First parent transformation
// 3. Second parent transformation
// 4. Stack transformation
res4Transformation1 := func(args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
if args.Name == "res4Child" {
props := args.Props.(*testResourceInputs)
props.Foo = pulumi.String("baz1")
return &pulumi.ResourceTransformationResult{
Props: props,
Opts: args.Opts,
}
}
return nil
}
res4Transformation2 := func(args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
if args.Name == "res4Child" {
props := args.Props.(*testResourceInputs)
props.Foo = pulumi.String("baz2")
return &pulumi.ResourceTransformationResult{
Props: props,
Opts: args.Opts,
}
}
return nil
}
assert.NoError(t, newComponent(ctx, "res4",
pulumi.Transformations([]pulumi.ResourceTransformation{res4Transformation1, res4Transformation2})))
return nil
})
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
}
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
_ []Event, res result.Result) result.Result {
foundRes1 := false
foundRes2 := false
foundRes2Child := false
foundRes3 := false
foundRes4Child := false
// foundRes5Child1 := false
for _, res := range entries.Snap(target.Snapshot).Resources {
// "res1" has a transformation which adds additionalSecretOutputs
if res.URN.Name() == "res1" {
foundRes1 = true
assert.Equal(t, res.Type, tokens.Type("pkgA:m:typA"))
assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("output"))
}
// "res2" has a transformation which adds additionalSecretOutputs to it's "child"
if res.URN.Name() == "res2" {
foundRes2 = true
assert.Equal(t, res.Type, tokens.Type("pkgA:m:typA"))
assert.NotContains(t, res.AdditionalSecretOutputs, resource.PropertyKey("output"))
}
if res.URN.Name() == "res2Child" {
foundRes2Child = true
assert.Equal(t, res.Parent.Name(), tokens.QName("res2"))
assert.Equal(t, res.Type, tokens.Type("pkgA:m:typA"))
assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("output"))
assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("output2"))
}
// "res3" is impacted by a global stack transformation which sets
// Foo to "baz"
if res.URN.Name() == "res3" {
foundRes3 = true
assert.Equal(t, "baz", res.Inputs["foo"].StringValue())
assert.Len(t, res.Aliases, 0)
}
// "res4" is impacted by two component parent transformations which set
// Foo to "baz1" and then "baz2" and also a global stack
// transformation which sets optionalDefault to "baz". The end
// result should be "baz".
if res.URN.Name() == "res4Child" {
foundRes4Child = true
assert.Equal(t, res.Parent.Name(), tokens.QName("res4"))
assert.Equal(t, "baz", res.Inputs["foo"].StringValue())
}
}
assert.True(t, foundRes1)
assert.True(t, foundRes2)
assert.True(t, foundRes2Child)
assert.True(t, foundRes3)
assert.True(t, foundRes4Child)
return res
},
}}
p.Run(t, nil)
}
// This test validates the wiring of the IgnoreChanges prop in the go SDK.
// It doesn't attempt to validate underlying behavior.
func TestIgnoreChangesGolangLifecycle(t *testing.T) {
var expectedIgnoreChanges []string
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
},
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
// just verify that the IgnoreChanges prop made it through
assert.Equal(t, expectedIgnoreChanges, ignoreChanges)
return plugin.DiffResult{}, nil
},
}, nil
}),
}
setupAndRunProgram := func(ignoreChanges []string) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{
Project: info.Project,
Stack: info.Stack,
Parallel: info.Parallel,
DryRun: info.DryRun,
MonitorAddr: info.MonitorAddress,
})
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
var res pulumi.CustomResourceState
err := ctx.RegisterResource("pkgA:m:typA", "resA", nil, &res, pulumi.IgnoreChanges(ignoreChanges))
assert.NoError(t, err)
return nil
})
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, event := range events {
if event.Type == ResourcePreEvent {
payload := event.Payload().(ResourcePreEventPayload)
assert.Equal(t, []deploy.StepOp{deploy.OpCreate}, []deploy.StepOp{payload.Metadata.Op})
}
}
return res
},
},
},
}
return p.Run(t, nil)
}
// ignore changes specified
ignoreChanges := []string{"b"}
setupAndRunProgram(ignoreChanges)
// ignore changes empty
ignoreChanges = []string{}
setupAndRunProgram(ignoreChanges)
}
func TestExplicitDeleteBeforeReplaceGoSDK(t *testing.T) {
p := &TestPlan{}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["foo"].DeepEquals(news["foo"]) {
return plugin.DiffResult{
ReplaceKeys: []resource.PropertyKey{"foo"},
DeleteBeforeReplace: true,
}, nil
}
return plugin.DiffResult{}, nil
},
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["foo"].DeepEquals(news["foo"]) {
return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"foo"}}, nil
}
return plugin.DiffResult{}, nil
},
}, nil
}),
}
inputsA := &testResourceInputs{Foo: pulumi.String("foo")}
dbrValue, dbrA := true, (*bool)(nil)
getDbr := func() bool {
if dbrA == nil {
return false
}
return *dbrA
}
var stackURN, provURN, urnA resource.URN = "urn:pulumi:test::test::pulumi:pulumi:Stack::test-test",
"urn:pulumi:test::test::pulumi:providers:pkgA::provA", "urn:pulumi:test::test::pkgA:m:typA::resA"
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{
Project: info.Project,
Stack: info.Stack,
Parallel: info.Parallel,
DryRun: info.DryRun,
MonitorAddr: info.MonitorAddress,
})
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
provider := &pulumi.ProviderResourceState{}
err := ctx.RegisterResource(string(providers.MakeProviderType("pkgA")), "provA", nil, provider)
assert.NoError(t, err)
var res pulumi.CustomResourceState
err = ctx.RegisterResource("pkgA:m:typA", "resA", inputsA, &res,
pulumi.Provider(provider), pulumi.DeleteBeforeReplace(getDbr()))
assert.NoError(t, err)
return nil
})
})
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Steps = []TestStep{{Op: Update}}
snap := p.Run(t, nil)
// Change the value of resA.A. Should create before replace
inputsA.Foo = pulumi.String("bar")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: stackURN},
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpCreateReplacement, URN: urnA},
{Op: deploy.OpReplace, URN: urnA},
{Op: deploy.OpDeleteReplaced, URN: urnA},
}, SuccessfulSteps(entries))
return res
},
}}
snap = p.Run(t, snap)
// Change the registration of resA such that it requires delete-before-replace and change the value of resA.A.
// replacement should be delete-before-replace.
dbrA, inputsA.Foo = &dbrValue, pulumi.String("baz")
p.Steps = []TestStep{{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpSame, URN: stackURN},
{Op: deploy.OpSame, URN: provURN},
{Op: deploy.OpDeleteReplaced, URN: urnA},
{Op: deploy.OpReplace, URN: urnA},
{Op: deploy.OpCreateReplacement, URN: urnA},
}, SuccessfulSteps(entries))
return res
},
}}
p.Run(t, snap)
}
func TestReadResourceGolangLifecycle(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
assert.Equal(t, resource.ID("someId"), id)
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
},
}, nil
}),
}
var stackURN, defaultProviderURN, urnA resource.URN = "urn:pulumi:test::test::pulumi:pulumi:Stack::test-test",
"urn:pulumi:test::test::pulumi:providers:pkgA::default", "urn:pulumi:test::test::pkgA:m:typA::resA"
setupAndRunProgram := func() *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{
Project: info.Project,
Stack: info.Stack,
Parallel: info.Parallel,
DryRun: info.DryRun,
MonitorAddr: info.MonitorAddress,
})
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
var res pulumi.CustomResourceState
err := ctx.ReadResource("pkgA:m:typA", "resA", pulumi.ID("someId"), nil, &res)
assert.NoError(t, err)
return nil
})
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
AssertSameSteps(t, []StepSummary{
{Op: deploy.OpCreate, URN: stackURN},
{Op: deploy.OpCreate, URN: defaultProviderURN},
{Op: deploy.OpRead, URN: urnA},
}, SuccessfulSteps(entries))
return res
},
},
},
}
return p.Run(t, nil)
}
setupAndRunProgram()
}
// ensures that RegisterResource, ReadResource (TODO https://github.com/pulumi/pulumi/issues/3562),
// and Invoke all respect the provider hierarchy
// most specific providers are used first 1. resource.provider, 2. resource.providers, 3. resource.parent.providers
func TestProviderInheritanceGolangLifecycle(t *testing.T) {
type invokeArgs struct {
Bang string `pulumi:"bang"`
Bar string `pulumi:"bar"`
}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
v := &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
},
}
v.InvokeF = func(tok tokens.ModuleMember,
inputs resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
assert.True(t, v.Config.DeepEquals(inputs))
return nil, nil, nil
}
return v, nil
}),
deploytest.NewProviderLoader("pkgB", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
v := &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
},
}
v.InvokeF = func(tok tokens.ModuleMember,
inputs resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
assert.True(t, v.Config.DeepEquals(inputs))
return nil, nil, nil
}
return v, nil
}),
}
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{
Project: info.Project,
Stack: info.Stack,
Parallel: info.Parallel,
DryRun: info.DryRun,
MonitorAddr: info.MonitorAddress,
})
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
// register a couple of providers, pass in some props that we can use to indentify it during invoke
var providerA pulumi.ProviderResourceState
err := ctx.RegisterResource(string(providers.MakeProviderType("pkgA")), "prov1",
&testResourceInputs{
Foo: pulumi.String("1"),
}, &providerA)
assert.NoError(t, err)
var providerB pulumi.ProviderResourceState
err = ctx.RegisterResource(string(providers.MakeProviderType("pkgB")), "prov2",
&testResourceInputs{
Bar: pulumi.String("2"),
Bang: pulumi.String(""),
}, &providerB)
assert.NoError(t, err)
var providerBOverride pulumi.ProviderResourceState
err = ctx.RegisterResource(string(providers.MakeProviderType("pkgB")), "prov3",
&testResourceInputs{
Bar: pulumi.String(""),
Bang: pulumi.String("3"),
}, &providerBOverride)
assert.NoError(t, err)
parentProviders := make(map[string]pulumi.ProviderResource)
parentProviders["pkgA"] = &providerA
parentProviders["pkgB"] = &providerB
// create a parent resource that uses provider map
var parentResource pulumi.CustomResourceState
err = ctx.RegisterResource("pkgA:m:typA", "resA", nil, &parentResource, pulumi.ProviderMap(parentProviders))
assert.NoError(t, err)
// parent uses specified provider from map
parentResultProvider := parentResource.GetProvider("pkgA:m:typA")
assert.Equal(t, &providerA, parentResultProvider)
// create a child resource
var childResource pulumi.CustomResourceState
err = ctx.RegisterResource("pkgB:m:typB", "resBChild", nil, &childResource, pulumi.Parent(&parentResource))
assert.NoError(t, err)
// child uses provider value from parent
childResultProvider := childResource.GetProvider("pkgB:m:typB")
assert.Equal(t, &providerB, childResultProvider)
// create a child with a provider specified
var childWithOverride pulumi.CustomResourceState
err = ctx.RegisterResource("pkgB:m:typB", "resBChildOverride", nil, &childWithOverride,
pulumi.Parent(&parentResource), pulumi.Provider(&providerBOverride))
assert.NoError(t, err)
// child uses the specified provider, and not the provider from the parent
childWithOverrideProvider := childWithOverride.GetProvider("pkgB:m:typB")
assert.Equal(t, &providerBOverride, childWithOverrideProvider)
// pass in a fake ID
testID := pulumi.ID("testID")
// read a resource that uses provider map
err = ctx.ReadResource("pkgA:m:typA", "readResA", testID, nil, &parentResource, pulumi.ProviderMap(parentProviders))
assert.NoError(t, err)
// parent uses specified provider from map
parentResultProvider = parentResource.GetProvider("pkgA:m:typA")
assert.Equal(t, &providerA, parentResultProvider)
// read a child resource
err = ctx.ReadResource("pkgB:m:typB", "readResBChild", testID, nil, &childResource, pulumi.Parent(&parentResource))
assert.NoError(t, err)
// child uses provider value from parent
childResultProvider = childResource.GetProvider("pkgB:m:typB")
assert.Equal(t, &providerB, childResultProvider)
// read a child with a provider specified
err = ctx.ReadResource("pkgB:m:typB", "readResBChildOverride", testID, nil, &childWithOverride,
pulumi.Parent(&parentResource), pulumi.Provider(&providerBOverride))
assert.NoError(t, err)
// child uses the specified provider, and not the provider from the parent
childWithOverrideProvider = childWithOverride.GetProvider("pkgB:m:typB")
assert.Equal(t, &providerBOverride, childWithOverrideProvider)
// invoke with specific provider
var invokeResult struct{}
err = ctx.Invoke("pkgB:do:something", invokeArgs{
Bang: "3",
}, &invokeResult, pulumi.Provider(&providerBOverride))
assert.NoError(t, err)
// invoke with parent
err = ctx.Invoke("pkgB:do:something", invokeArgs{
Bar: "2",
}, &invokeResult, pulumi.Parent(&parentResource))
assert.NoError(t, err)
// invoke with parent and provider
err = ctx.Invoke("pkgB:do:something", invokeArgs{
Bang: "3",
}, &invokeResult, pulumi.Parent(&parentResource), pulumi.Provider(&providerBOverride))
assert.NoError(t, err)
return nil
})
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{{Op: Update}},
}
p.Run(t, nil)
}
func TestSingleComponentDefaultProviderLifecycle(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
construct := func(monitor *deploytest.ResourceMonitor,
typ, name string, parent resource.URN, inputs resource.PropertyMap,
options plugin.ConstructOptions) (plugin.ConstructResult, error) {
urn, _, _, err := monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{
Parent: parent,
Aliases: options.Aliases,
Protect: options.Protect,
})
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{
Parent: urn,
})
assert.NoError(t, err)
outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
err = monitor.RegisterResourceOutputs(urn, outs)
assert.NoError(t, err)
return plugin.ConstructResult{
URN: urn,
Outputs: outs,
}, nil
}
return &deploytest.Provider{
ConstructF: construct,
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
Remote: true,
})
assert.NoError(t, err)
assert.Equal(t, resource.PropertyMap{
"foo": resource.NewStringProperty("bar"),
}, state)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: MakeBasicLifecycleSteps(t, 3),
}
p.Run(t, nil)
}
func TestResourceReferences(t *testing.T) {
var urnA resource.URN
var idA resource.ID
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
v := &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap,
timeout float64, preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
if urn.Name() == "resB" {
propA, ok := news["resA"]
assert.True(t, ok)
assert.Equal(t, resource.MakeResourceReference(urnA, idA, ""), propA)
}
return "created-id", news, resource.StatusOK, nil
},
ReadF: func(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
},
}
return v, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
var err error
urnA, idA, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
ins := resource.PropertyMap{
"resA": resource.MakeResourceReference(urnA, idA, ""),
}
_, _, props, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
Inputs: ins,
})
assert.NoError(t, err)
assert.Equal(t, ins, props)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: MakeBasicLifecycleSteps(t, 3),
}
p.Run(t, nil)
}
type updateContext struct {
*deploytest.ResourceMonitor
resmon chan *deploytest.ResourceMonitor
programErr chan error
snap chan *deploy.Snapshot
updateResult chan result.Result
}
func startUpdate(host plugin.Host) (*updateContext, error) {
ctx := &updateContext{
resmon: make(chan *deploytest.ResourceMonitor),
programErr: make(chan error),
snap: make(chan *deploy.Snapshot),
updateResult: make(chan result.Result),
}
stop := make(chan bool)
port, _, err := rpcutil.Serve(0, stop, []func(*grpc.Server) error{
func(srv *grpc.Server) error {
pulumirpc.RegisterLanguageRuntimeServer(srv, ctx)
return nil
},
}, nil)
if err != nil {
return nil, err
}
p := &TestPlan{
Options: UpdateOptions{Host: host},
Runtime: "client",
RuntimeOptions: map[string]interface{}{
"address": fmt.Sprintf("127.0.0.1:%d", port),
},
}
go func() {
snap, res := TestOp(Update).Run(p.GetProject(), p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
ctx.snap <- snap
close(ctx.snap)
ctx.updateResult <- res
close(ctx.updateResult)
stop <- true
}()
ctx.ResourceMonitor = <-ctx.resmon
return ctx, nil
}
func (ctx *updateContext) Finish(err error) (*deploy.Snapshot, result.Result) {
ctx.programErr <- err
close(ctx.programErr)
return <-ctx.snap, <-ctx.updateResult
}
func (ctx *updateContext) GetRequiredPlugins(_ context.Context,
req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) {
return &pulumirpc.GetRequiredPluginsResponse{}, nil
}
func (ctx *updateContext) Run(_ context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) {
// Connect to the resource monitor and create an appropriate client.
conn, err := grpc.Dial(
req.MonitorAddress,
grpc.WithInsecure(),
rpcutil.GrpcChannelOptions(),
)
if err != nil {
return nil, errors.Wrapf(err, "could not connect to resource monitor")
}
defer contract.IgnoreClose(conn)
// Fire up a resource monitor client
ctx.resmon <- deploytest.NewResourceMonitor(pulumirpc.NewResourceMonitorClient(conn))
close(ctx.resmon)
// Wait for the program to terminate.
if err := <-ctx.programErr; err != nil {
return &pulumirpc.RunResponse{Error: err.Error()}, nil
}
return &pulumirpc.RunResponse{}, nil
}
func (ctx *updateContext) GetPluginInfo(_ context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
return &pulumirpc.PluginInfo{
Version: "1.0.0",
}, nil
}
func TestLanguageClient(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
update, err := startUpdate(deploytest.NewPluginHost(nil, nil, nil, loaders...))
if err != nil {
t.Fatalf("failed to start update: %v", err)
}
// Register resources, etc.
_, _, _, err = update.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
snap, res := update.Finish(nil)
assert.Nil(t, res)
assert.Len(t, snap.Resources, 2)
}