mirror of https://github.com/pulumi/pulumi.git
699 lines
23 KiB
Go
699 lines
23 KiB
Go
// Copyright 2020-2024, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package lifecycletest
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/maps"
|
|
|
|
. "github.com/pulumi/pulumi/pkg/v3/engine" //nolint:revive
|
|
lt "github.com/pulumi/pulumi/pkg/v3/engine/lifecycletest/framework"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
)
|
|
|
|
type propertyDependencies map[resource.PropertyKey][]resource.URN
|
|
|
|
var complexTestDependencyGraphNames = []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"}
|
|
|
|
func generateComplexTestDependencyGraph(
|
|
t *testing.T, p *lt.TestPlan,
|
|
) ([]resource.URN, *deploy.Snapshot, deploytest.LanguageRuntimeFactory) {
|
|
resType := tokens.Type("pkgA:m:typA")
|
|
|
|
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 {
|
|
return newResource(urn, "", id, provider, dependencies, propertyDeps, outputs, true)
|
|
}
|
|
|
|
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),
|
|
},
|
|
}
|
|
assert.NoError(t, old.VerifyIntegrity())
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
register := func(urn resource.URN, provider string, inputs resource.PropertyMap) resource.ID {
|
|
resp, err := monitor.RegisterResource(urn.Type(), urn.Name(), true, deploytest.ResourceOptions{
|
|
Provider: provider,
|
|
Inputs: inputs,
|
|
})
|
|
assert.NoError(t, err)
|
|
return resp.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, programF
|
|
}
|
|
|
|
func TestDeleteBeforeReplace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// 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, programF := 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(
|
|
_ context.Context,
|
|
req plugin.DiffConfigRequest,
|
|
) (plugin.DiffResult, error) {
|
|
if !req.OldOutputs["A"].DeepEquals(req.NewInputs["A"]) {
|
|
return plugin.DiffResult{
|
|
ReplaceKeys: []resource.PropertyKey{"A"},
|
|
DeleteBeforeReplace: true,
|
|
}, nil
|
|
}
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if !req.OldOutputs["A"].DeepEquals(req.NewInputs["A"]) {
|
|
return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil
|
|
}
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p.Options.T = t
|
|
p.Steps = []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: false,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
}}
|
|
|
|
p.Run(t, old)
|
|
}
|
|
|
|
func TestPropertyDependenciesAdapter(t *testing.T) {
|
|
t.Parallel()
|
|
// 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
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
register := func(name string, inputs resource.PropertyMap, inputDeps propertyDependencies,
|
|
dependencies []resource.URN,
|
|
) resource.URN {
|
|
resp, err := monitor.RegisterResource(resType, name, true, deploytest.ResourceOptions{
|
|
Inputs: inputs,
|
|
Dependencies: dependencies,
|
|
PropertyDeps: inputDeps,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return resp.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
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.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.ElementsMatch(t, []resource.URN{urnA, urnB}, res.Dependencies)
|
|
assert.ElementsMatch(t, []resource.PropertyKey{"A", "B"}, maps.Keys(res.PropertyDependencies))
|
|
assert.ElementsMatch(t, res.Dependencies, res.PropertyDependencies["A"])
|
|
assert.ElementsMatch(t, res.Dependencies, res.PropertyDependencies["B"])
|
|
case urnD:
|
|
assert.ElementsMatch(t, []resource.URN{urnA, urnB, urnC}, res.Dependencies)
|
|
assert.ElementsMatch(t, []resource.PropertyKey{"A", "B"}, maps.Keys(res.PropertyDependencies))
|
|
assert.ElementsMatch(t, []resource.URN{urnB}, res.PropertyDependencies["A"])
|
|
assert.ElementsMatch(t, []resource.URN{urnA, urnC}, res.PropertyDependencies["B"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExplicitDeleteBeforeReplace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := <.TestPlan{}
|
|
|
|
dbrDiff := false
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if !req.OldOutputs["A"].DeepEquals(req.NewInputs["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
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
var resp *deploytest.RegisterResourceResponse
|
|
resp, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true)
|
|
assert.NoError(t, err)
|
|
provURN, provID = resp.URN, resp.ID
|
|
|
|
if provID == "" {
|
|
provID = providers.UnknownID
|
|
}
|
|
provRef, err := providers.NewReference(provURN, provID)
|
|
assert.NoError(t, err)
|
|
provA := provRef.String()
|
|
|
|
respA, err := monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
|
|
Provider: provA,
|
|
Inputs: inputsA,
|
|
DeleteBeforeReplace: dbrA,
|
|
})
|
|
assert.NoError(t, err)
|
|
urnA = respA.URN
|
|
|
|
inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}}
|
|
respB, err := monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
|
|
Provider: provA,
|
|
Inputs: inputsB,
|
|
Dependencies: []resource.URN{urnA},
|
|
PropertyDeps: inputDepsB,
|
|
})
|
|
assert.NoError(t, err)
|
|
urnB = respB.URN
|
|
|
|
return nil
|
|
})
|
|
|
|
p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p.Options.T = t
|
|
p.Steps = []lt.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 = []lt.TestStep{{
|
|
Op: Update,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
}}
|
|
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 = []lt.TestStep{{
|
|
Op: Update,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
}}
|
|
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 = []lt.TestStep{{
|
|
Op: Update,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
}}
|
|
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 = []lt.TestStep{{
|
|
Op: Update,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
}}
|
|
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 = []lt.TestStep{{
|
|
Op: Update,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
}}
|
|
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 = []lt.TestStep{{
|
|
Op: Update,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
}}
|
|
p.Run(t, snap)
|
|
}
|
|
|
|
func TestDependencyChangeDBR(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := <.TestPlan{}
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if !req.OldOutputs["A"].DeepEquals(req.NewInputs["A"]) {
|
|
return plugin.DiffResult{
|
|
ReplaceKeys: []resource.PropertyKey{"A"},
|
|
DeleteBeforeReplace: true,
|
|
}, nil
|
|
}
|
|
if !req.OldOutputs["B"].DeepEquals(req.NewInputs["B"]) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
}, nil
|
|
}
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: 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
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputsA,
|
|
})
|
|
assert.NoError(t, err)
|
|
urnA = resp.URN
|
|
|
|
inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}}
|
|
resp, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
|
|
Inputs: inputsB,
|
|
Dependencies: []resource.URN{urnA},
|
|
PropertyDeps: inputDepsB,
|
|
})
|
|
assert.NoError(t, err)
|
|
urnB = resp.URN
|
|
|
|
return nil
|
|
})
|
|
|
|
p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p.Options.T = t
|
|
p.Steps = []lt.TestStep{{Op: Update}}
|
|
snap := p.Run(t, nil)
|
|
|
|
inputsA["A"] = resource.NewStringProperty("bar")
|
|
programF = deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
|
|
Inputs: inputsB,
|
|
})
|
|
assert.NoError(t, err)
|
|
urnB = resp.URN
|
|
|
|
resp, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputsA,
|
|
})
|
|
assert.NoError(t, err)
|
|
urnA = resp.URN
|
|
|
|
return nil
|
|
})
|
|
|
|
p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p.Steps = []lt.TestStep{
|
|
{
|
|
Op: Update,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
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 err
|
|
},
|
|
},
|
|
}
|
|
p.Run(t, snap)
|
|
}
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/15763. Check that if a resource gets implicated in a
|
|
// replacement chain that it fails if the resource is protected.
|
|
func TestDBRProtect(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if !req.OldOutputs["A"].DeepEquals(req.NewInputs["A"]) {
|
|
return plugin.DiffResult{
|
|
ReplaceKeys: []resource.PropertyKey{"A"},
|
|
DeleteBeforeReplace: true,
|
|
}, nil
|
|
}
|
|
if !req.OldOutputs["B"].DeepEquals(req.NewInputs["B"]) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
}, nil
|
|
}
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: 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"})
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
respA, err := monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputsA,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {respA.URN}}
|
|
_, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
|
|
Inputs: inputsB,
|
|
Dependencies: []resource.URN{respA.URN},
|
|
PropertyDeps: inputDepsB,
|
|
Protect: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
options := lt.TestUpdateOptions{T: t, HostF: hostF}
|
|
p := <.TestPlan{}
|
|
|
|
project := p.GetProject()
|
|
|
|
// First update just create the two resources.
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), options, false, p.BackendClient, nil, "0")
|
|
require.NoError(t, err)
|
|
assert.Len(t, snap.Resources, 3)
|
|
|
|
// Update A to trigger a replace this should error because of the protect flag on B.
|
|
inputsA["A"] = resource.NewStringProperty("bar")
|
|
_, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), options, false, p.BackendClient, nil, "1")
|
|
assert.ErrorContains(t, err, "unable to replace resource \"urn:pulumi:test::test::pkgA:index:typ::resB\""+
|
|
" as part of replacing \"urn:pulumi:test::test::pkgA:index:typ::resA\" as it is currently marked for protection.")
|
|
|
|
// Remove the protect flag and try again
|
|
assert.Equal(t, snap.Resources[2].Protect, true)
|
|
snap.Resources[2].Protect = false
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), options, false, p.BackendClient, nil, "2")
|
|
require.NoError(t, err)
|
|
assert.Len(t, snap.Resources, 3)
|
|
}
|