Compare commits
5 commits
master
...
t0yv0/6283
Author | SHA1 | Date | |
---|---|---|---|
|
b52bb7dd4f | ||
|
d99bc5e908 | ||
|
c651d0ab9b | ||
|
0f1893222a | ||
|
2fd7069a09 |
|
@ -5,3 +5,6 @@
|
||||||
|
|
||||||
- [programgen/go] - Don't change imported resource names.
|
- [programgen/go] - Don't change imported resource names.
|
||||||
[#8353](https://github.com/pulumi/pulumi/pull/8353)
|
[#8353](https://github.com/pulumi/pulumi/pull/8353)
|
||||||
|
|
||||||
|
- [engine] - Compute dependents correctly during targeted deletes.
|
||||||
|
- [#8360](https://github.com/pulumi/pulumi/pull/8360)
|
||||||
|
|
|
@ -33,15 +33,21 @@ func TestDestroyTarget(t *testing.T) {
|
||||||
destroySpecificTargets(
|
destroySpecificTargets(
|
||||||
t, []string{"A"}, true, /*targetDependents*/
|
t, []string{"A"}, true, /*targetDependents*/
|
||||||
func(urns []resource.URN, deleted map[resource.URN]bool) {
|
func(urns []resource.URN, deleted map[resource.URN]bool) {
|
||||||
// when deleting 'A' we expect A, B, C, E, F, and K to be deleted
|
// when deleting 'A' we expect A, B, C, D, E, F, G, H, I, J, K, and L to be deleted
|
||||||
names := complexTestDependencyGraphNames
|
names := complexTestDependencyGraphNames
|
||||||
assert.Equal(t, map[resource.URN]bool{
|
assert.Equal(t, map[resource.URN]bool{
|
||||||
pickURN(t, urns, names, "A"): true,
|
pickURN(t, urns, names, "A"): true,
|
||||||
pickURN(t, urns, names, "B"): true,
|
pickURN(t, urns, names, "B"): true,
|
||||||
pickURN(t, urns, names, "C"): true,
|
pickURN(t, urns, names, "C"): true,
|
||||||
|
pickURN(t, urns, names, "D"): true,
|
||||||
pickURN(t, urns, names, "E"): true,
|
pickURN(t, urns, names, "E"): true,
|
||||||
pickURN(t, urns, names, "F"): true,
|
pickURN(t, urns, names, "F"): true,
|
||||||
|
pickURN(t, urns, names, "G"): true,
|
||||||
|
pickURN(t, urns, names, "H"): true,
|
||||||
|
pickURN(t, urns, names, "I"): true,
|
||||||
|
pickURN(t, urns, names, "J"): true,
|
||||||
pickURN(t, urns, names, "K"): true,
|
pickURN(t, urns, names, "K"): true,
|
||||||
|
pickURN(t, urns, names, "L"): true,
|
||||||
}, deleted)
|
}, deleted)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -772,23 +772,44 @@ func (sg *stepGenerator) GenerateDeletes(targetsOpt map[resource.URN]bool) ([]St
|
||||||
return dels, nil
|
return dels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sg *stepGenerator) getTargetIncludingChildren(target resource.URN) map[resource.URN]bool {
|
// getTargetDependents returns the (transitive) set of dependents on the target resources.
|
||||||
allTargets := make(map[resource.URN]bool)
|
// This includes both implicit and explicit dependents in the DAG itself, as well as children.
|
||||||
allTargets[target] = true
|
func (sg *stepGenerator) getTargetDependents(targetsOpt map[resource.URN]bool) map[resource.URN]bool {
|
||||||
|
// Seed the list with the initial set of targets.
|
||||||
// The list of resources is a topological sort of the reverse-dependency graph, so any
|
var frontier []*resource.State
|
||||||
// resource R will appear in a list before its dependents and its children. We can use this
|
|
||||||
// to our advantage here and find all of a resource's children via a linear scan of the list
|
|
||||||
// starting from R.
|
|
||||||
for _, res := range sg.deployment.prev.Resources {
|
for _, res := range sg.deployment.prev.Resources {
|
||||||
if _, has := allTargets[res.Parent]; has {
|
if _, has := targetsOpt[res.URN]; has {
|
||||||
allTargets[res.URN] = true
|
frontier = append(frontier, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allTargets
|
// Produce a dependency graph of resources.
|
||||||
|
dg := graph.NewDependencyGraph(sg.deployment.prev.Resources)
|
||||||
|
|
||||||
|
// Now accumulate a list of targets that are implicated because they depend upon the targets.
|
||||||
|
targets := make(map[resource.URN]bool)
|
||||||
|
for len(frontier) > 0 {
|
||||||
|
// Pop the next to explore, mark it, and skip any we've already seen.
|
||||||
|
next := frontier[0]
|
||||||
|
frontier = frontier[1:]
|
||||||
|
if _, has := targets[next.URN]; has {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targets[next.URN] = true
|
||||||
|
|
||||||
|
// Compute the set of resources depending on this one, either implicitly, explicitly,
|
||||||
|
// or because it is a child resource. Add them to the frontier to keep exploring.
|
||||||
|
deps := dg.DependingOn(next, targets, true)
|
||||||
|
frontier = append(frontier, deps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determineAllowedResourcesToDeleteFromTargets computes the full (transitive) closure of resources
|
||||||
|
// that need to be deleted to permit the full list of targetsOpt resources to be deleted. This list
|
||||||
|
// will include the targetsOpt resources, but may contain more than just that, if there are dependent
|
||||||
|
// or child resources that require the targets to exist (and so are implicated in the deletion).
|
||||||
func (sg *stepGenerator) determineAllowedResourcesToDeleteFromTargets(
|
func (sg *stepGenerator) determineAllowedResourcesToDeleteFromTargets(
|
||||||
targetsOpt map[resource.URN]bool) (map[resource.URN]bool, result.Result) {
|
targetsOpt map[resource.URN]bool) (map[resource.URN]bool, result.Result) {
|
||||||
|
|
||||||
|
@ -797,21 +818,14 @@ func (sg *stepGenerator) determineAllowedResourcesToDeleteFromTargets(
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
targetsIncludingChildren := make(map[resource.URN]bool)
|
// Produce a map of targets and their dependents, including explicit and implicit
|
||||||
|
// DAG dependencies, as well as children (transitively).
|
||||||
// Include all the children of each target.
|
targets := sg.getTargetDependents(targetsOpt)
|
||||||
for target := range targetsOpt {
|
|
||||||
allTargets := sg.getTargetIncludingChildren(target)
|
|
||||||
for child := range allTargets {
|
|
||||||
targetsIncludingChildren[child] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.V(7).Infof("Planner was asked to only delete/update '%v'", targetsOpt)
|
logging.V(7).Infof("Planner was asked to only delete/update '%v'", targetsOpt)
|
||||||
resourcesToDelete := make(map[resource.URN]bool)
|
resourcesToDelete := make(map[resource.URN]bool)
|
||||||
|
|
||||||
// Now actually use all the requested targets to figure out the exact set to delete.
|
// Now actually use all the requested targets to figure out the exact set to delete.
|
||||||
for target := range targetsIncludingChildren {
|
for target := range targets {
|
||||||
current := sg.deployment.olds[target]
|
current := sg.deployment.olds[target]
|
||||||
if current == nil {
|
if current == nil {
|
||||||
// user specified a target that didn't exist. they will have already gotten a warning
|
// user specified a target that didn't exist. they will have already gotten a warning
|
||||||
|
@ -1395,7 +1409,7 @@ func (sg *stepGenerator) calculateDependentReplacements(root *resource.State) ([
|
||||||
// that have already been registered must not depend on the root. Thus, we ignore these resources if they are
|
// that have already been registered must not depend on the root. Thus, we ignore these resources if they are
|
||||||
// encountered while walking the old dependency graph to determine the set of dependents.
|
// encountered while walking the old dependency graph to determine the set of dependents.
|
||||||
impossibleDependents := sg.urns
|
impossibleDependents := sg.urns
|
||||||
for _, d := range sg.deployment.depGraph.DependingOn(root, impossibleDependents) {
|
for _, d := range sg.deployment.depGraph.DependingOn(root, impossibleDependents, false) {
|
||||||
replace, keys, res := requiresReplacement(d)
|
replace, keys, res := requiresReplacement(d)
|
||||||
if res != nil {
|
if res != nil {
|
||||||
return nil, res
|
return nil, res
|
||||||
|
|
|
@ -41,7 +41,7 @@ func DeleteResource(snapshot *deploy.Snapshot, condemnedRes *resource.State) err
|
||||||
}
|
}
|
||||||
|
|
||||||
dg := graph.NewDependencyGraph(snapshot.Resources)
|
dg := graph.NewDependencyGraph(snapshot.Resources)
|
||||||
dependencies := dg.DependingOn(condemnedRes, nil)
|
dependencies := dg.DependingOn(condemnedRes, nil, false)
|
||||||
if len(dependencies) != 0 {
|
if len(dependencies) != 0 {
|
||||||
return ResourceHasDependenciesError{Condemned: condemnedRes, Dependencies: dependencies}
|
return ResourceHasDependenciesError{Condemned: condemnedRes, Dependencies: dependencies}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,8 @@ type DependencyGraph struct {
|
||||||
// order with respect to the snapshot dependency graph.
|
// order with respect to the snapshot dependency graph.
|
||||||
//
|
//
|
||||||
// The time complexity of DependingOn is linear with respect to the number of resources.
|
// The time complexity of DependingOn is linear with respect to the number of resources.
|
||||||
func (dg *DependencyGraph) DependingOn(res *resource.State, ignore map[resource.URN]bool) []*resource.State {
|
func (dg *DependencyGraph) DependingOn(res *resource.State,
|
||||||
|
ignore map[resource.URN]bool, includeChildren bool) []*resource.State {
|
||||||
// This implementation relies on the detail that snapshots are stored in a valid
|
// This implementation relies on the detail that snapshots are stored in a valid
|
||||||
// topological order.
|
// topological order.
|
||||||
var dependents []*resource.State
|
var dependents []*resource.State
|
||||||
|
@ -31,17 +32,22 @@ func (dg *DependencyGraph) DependingOn(res *resource.State, ignore map[resource.
|
||||||
dependentSet[res.URN] = true
|
dependentSet[res.URN] = true
|
||||||
|
|
||||||
isDependent := func(candidate *resource.State) bool {
|
isDependent := func(candidate *resource.State) bool {
|
||||||
if ignore[candidate.URN] {
|
// Direct deps include explicit `Dependencies`,
|
||||||
return false
|
// provider, and parent (under `includeChildren=true`
|
||||||
}
|
// semantic).
|
||||||
|
directDeps := candidate.Dependencies
|
||||||
if candidate.Provider != "" {
|
if candidate.Provider != "" {
|
||||||
ref, err := providers.ParseReference(candidate.Provider)
|
ref, err := providers.ParseReference(candidate.Provider)
|
||||||
contract.Assert(err == nil)
|
contract.Assert(err == nil)
|
||||||
if dependentSet[ref.URN()] {
|
directDeps = append(directDeps, ref.URN())
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, dependency := range candidate.Dependencies {
|
if includeChildren && candidate.Parent != "" {
|
||||||
|
directDeps = append(directDeps, candidate.Parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are computing a transitive closure of direct
|
||||||
|
// deps; therefore check in `dependentSet`.
|
||||||
|
for _, dependency := range directDeps {
|
||||||
if dependentSet[dependency] {
|
if dependentSet[dependency] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,58 +63,58 @@ func TestBasicGraph(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
a, b, pB, c, d,
|
a, b, pB, c, d,
|
||||||
}, dg.DependingOn(pA, nil))
|
}, dg.DependingOn(pA, nil, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
b, pB, c, d,
|
b, pB, c, d,
|
||||||
}, dg.DependingOn(a, nil))
|
}, dg.DependingOn(a, nil, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
pB, c, d,
|
pB, c, d,
|
||||||
}, dg.DependingOn(b, nil))
|
}, dg.DependingOn(b, nil, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
c,
|
c,
|
||||||
}, dg.DependingOn(pB, nil))
|
}, dg.DependingOn(pB, nil, false))
|
||||||
|
|
||||||
assert.Nil(t, dg.DependingOn(c, nil))
|
assert.Nil(t, dg.DependingOn(c, nil, false))
|
||||||
assert.Nil(t, dg.DependingOn(d, nil))
|
assert.Nil(t, dg.DependingOn(d, nil, false))
|
||||||
|
|
||||||
assert.Nil(t, dg.DependingOn(pA, map[resource.URN]bool{
|
assert.Nil(t, dg.DependingOn(pA, map[resource.URN]bool{
|
||||||
a.URN: true,
|
a.URN: true,
|
||||||
b.URN: true,
|
b.URN: true,
|
||||||
}))
|
}, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
a, pB, c,
|
a, pB, c,
|
||||||
}, dg.DependingOn(pA, map[resource.URN]bool{
|
}, dg.DependingOn(pA, map[resource.URN]bool{
|
||||||
b.URN: true,
|
b.URN: true,
|
||||||
}))
|
}, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
b, pB, c, d,
|
b, pB, c, d,
|
||||||
}, dg.DependingOn(pA, map[resource.URN]bool{
|
}, dg.DependingOn(pA, map[resource.URN]bool{
|
||||||
a.URN: true,
|
a.URN: true,
|
||||||
}))
|
}, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
c,
|
c,
|
||||||
}, dg.DependingOn(a, map[resource.URN]bool{
|
}, dg.DependingOn(a, map[resource.URN]bool{
|
||||||
b.URN: true,
|
b.URN: true,
|
||||||
pB.URN: true,
|
pB.URN: true,
|
||||||
}))
|
}, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
pB, c,
|
pB, c,
|
||||||
}, dg.DependingOn(a, map[resource.URN]bool{
|
}, dg.DependingOn(a, map[resource.URN]bool{
|
||||||
b.URN: true,
|
b.URN: true,
|
||||||
}))
|
}, false))
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
d,
|
d,
|
||||||
}, dg.DependingOn(b, map[resource.URN]bool{
|
}, dg.DependingOn(b, map[resource.URN]bool{
|
||||||
pB.URN: true,
|
pB.URN: true,
|
||||||
}))
|
}, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests that we don't add the same node to the DependingOn set twice.
|
// Tests that we don't add the same node to the DependingOn set twice.
|
||||||
|
@ -133,7 +133,7 @@ func TestGraphNoDuplicates(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, []*resource.State{
|
assert.Equal(t, []*resource.State{
|
||||||
b, c, d,
|
b, c, d,
|
||||||
}, dg.DependingOn(a, nil))
|
}, dg.DependingOn(a, nil, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDependenciesOf(t *testing.T) {
|
func TestDependenciesOf(t *testing.T) {
|
||||||
|
@ -229,3 +229,41 @@ func TestDependenciesOfRemoteComponentsNoCycle(t *testing.T) {
|
||||||
assert.True(t, rDependencies[parent])
|
assert.True(t, rDependencies[parent])
|
||||||
assert.False(t, rDependencies[child])
|
assert.False(t, rDependencies[child])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewCustomResource(name string, provider *resource.State, deps ...resource.URN) *resource.State {
|
||||||
|
r := NewResource(name, provider, deps...)
|
||||||
|
r.Custom = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDependingOnIndirect(t *testing.T) {
|
||||||
|
var noProvider *resource.State
|
||||||
|
|
||||||
|
d2 := NewCustomResource("d2", noProvider)
|
||||||
|
d3 := NewCustomResource("d3", noProvider, d2.URN)
|
||||||
|
c3 := NewCustomResource("c3", noProvider)
|
||||||
|
c3.Parent = d3.URN
|
||||||
|
|
||||||
|
dg := NewDependencyGraph([]*resource.State{
|
||||||
|
d2, d3, c3,
|
||||||
|
})
|
||||||
|
|
||||||
|
for r := range dg.DependenciesOf(c3) {
|
||||||
|
t.Logf("dg.DependenciesOf(c3) includes %v", r.URN)
|
||||||
|
}
|
||||||
|
|
||||||
|
for r := range dg.DependenciesOf(d3) {
|
||||||
|
t.Logf("dg.DependenciesOf(d3) includes %v", r.URN)
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundC3 bool
|
||||||
|
for _, r := range dg.DependingOn(d2, nil, true) {
|
||||||
|
if r == c3 {
|
||||||
|
foundC3 = true
|
||||||
|
}
|
||||||
|
t.Logf("DependingOn(d2) includes %v", r.URN)
|
||||||
|
}
|
||||||
|
if !foundC3 {
|
||||||
|
t.Errorf("DependingOn(d2, nil) should have included c3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
name: delete_targets_many_deps
|
||||||
|
runtime: nodejs
|
47
tests/integration/targets/delete_targets_many_deps/index.ts
Normal file
47
tests/integration/targets/delete_targets_many_deps/index.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2021, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
|
import * as pulumi from "@pulumi/pulumi";
|
||||||
|
|
||||||
|
interface MyResourceArgs {
|
||||||
|
input?: pulumi.Input<string> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyResource extends pulumi.dynamic.Resource {
|
||||||
|
static latestId: number = 0;
|
||||||
|
|
||||||
|
constructor(name, args?: MyResourceArgs, opts?: pulumi.CustomResourceOptions) {
|
||||||
|
super({
|
||||||
|
async create(inputs: any) {
|
||||||
|
return { id: (MyResource.latestId++).toString() };
|
||||||
|
},
|
||||||
|
}, name, args || {}, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a chain of resources, such that attempting to delete
|
||||||
|
// one will fail due to numerous dependency violations. This includes
|
||||||
|
// both implicit and explicit, as well as parent/child, dependencies.
|
||||||
|
|
||||||
|
// A
|
||||||
|
// B (impl depends on A)
|
||||||
|
// C (expl depends on A)
|
||||||
|
// D (impl depends on B)
|
||||||
|
// E (expl depends on B)
|
||||||
|
// F (child of A)
|
||||||
|
// G (child of B)
|
||||||
|
// H (expl depends on A, B, impl depends on C, D, child of F)
|
||||||
|
|
||||||
|
const a = new MyResource("a");
|
||||||
|
const b = new MyResource("b", { input: a.urn });
|
||||||
|
const c = new MyResource("c", { }, { dependsOn: a });
|
||||||
|
const d = new MyResource("d", { input: b.urn });
|
||||||
|
const e = new MyResource("e", { }, { dependsOn: b });
|
||||||
|
const f = new MyResource("f", { }, { parent: a });
|
||||||
|
const g = new MyResource("g", { }, { parent: b });
|
||||||
|
const h = new MyResource("h",
|
||||||
|
{ input: pulumi.all(([c.urn, d.urn])).apply(([curn, _])=>curn) },
|
||||||
|
{
|
||||||
|
dependsOn: [a, b],
|
||||||
|
parent: f,
|
||||||
|
},
|
||||||
|
);
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "delete_targets_many_deps",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"main": "bin/index.js",
|
||||||
|
"typings": "bin/index.d.ts",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^2.5.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@pulumi/pulumi": "latest"
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
package ints
|
package ints
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -10,8 +11,10 @@ import (
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
||||||
ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
|
ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
||||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUntargetedCreateDuringTargetedUpdate(t *testing.T) {
|
func TestUntargetedCreateDuringTargetedUpdate(t *testing.T) {
|
||||||
|
@ -48,3 +51,56 @@ func TestUntargetedCreateDuringTargetedUpdate(t *testing.T) {
|
||||||
e.RunCommand("pulumi", "destroy", "--skip-preview", "--non-interactive", "--yes")
|
e.RunCommand("pulumi", "destroy", "--skip-preview", "--non-interactive", "--yes")
|
||||||
e.RunCommand("pulumi", "stack", "rm", "--yes")
|
e.RunCommand("pulumi", "stack", "rm", "--yes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteManyTargets(t *testing.T) {
|
||||||
|
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
|
||||||
|
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
e := ptesting.NewEnvironment(t)
|
||||||
|
defer func() {
|
||||||
|
if !t.Failed() {
|
||||||
|
e.DeleteEnvironment()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// First just spin up the project.
|
||||||
|
projName := "delete_targets_many_deps"
|
||||||
|
stackName, err := resource.NewUniqueHex("test-", 8, -1)
|
||||||
|
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
|
||||||
|
e.ImportDirectory(projName)
|
||||||
|
e.RunCommand("pulumi", "stack", "init", stackName)
|
||||||
|
e.RunCommand("yarn", "install")
|
||||||
|
e.RunCommand("yarn", "link", "@pulumi/pulumi")
|
||||||
|
e.RunCommand("pulumi", "up", "--non-interactive", "--skip-preview", "--yes")
|
||||||
|
|
||||||
|
// Create a handy mkURN func to create URNs for dynamic resources in this project/stack.
|
||||||
|
resourceType := tokens.Type("pulumi-nodejs:dynamic:Resource")
|
||||||
|
mkURNStr := func(resourceName tokens.QName, parentType tokens.Type) string {
|
||||||
|
return string(resource.NewURN(
|
||||||
|
tokens.QName(stackName), tokens.PackageName(projName), parentType, resourceType, resourceName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to destroy the root-most node. It should fail and the error text should
|
||||||
|
// mention every one of the nodes in the entire graph (since they all transitively depend on a).
|
||||||
|
stdout, _ := e.RunCommandExpectError("pulumi", "destroy", "--skip-preview", "--yes", "--non-interactive",
|
||||||
|
"--target", mkURNStr("a", ""))
|
||||||
|
assert.Contains(t, stdout, mkURNStr("b", ""))
|
||||||
|
assert.Contains(t, stdout, mkURNStr("c", ""))
|
||||||
|
assert.Contains(t, stdout, mkURNStr("d", ""))
|
||||||
|
assert.Contains(t, stdout, mkURNStr("e", ""))
|
||||||
|
assert.Contains(t, stdout, mkURNStr("f", resourceType))
|
||||||
|
assert.Contains(t, stdout, mkURNStr("g", resourceType))
|
||||||
|
|
||||||
|
// Destroy the leaf-most node. This should work just fine.
|
||||||
|
e.RunCommand("pulumi", "destroy", "--skip-preview", "--yes", "--non-interactive",
|
||||||
|
"--target", mkURNStr("h", tokens.Type(fmt.Sprintf("%[1]s$%[1]s", resourceType))))
|
||||||
|
|
||||||
|
// Finally, go back and try to delete the root-most node, but clean up the transitive closure.
|
||||||
|
e.RunCommand("pulumi", "destroy", "--skip-preview", "--yes", "--non-interactive",
|
||||||
|
"--target", mkURNStr("a", ""), "--target-dependents")
|
||||||
|
|
||||||
|
// Finally clean up the entire stack.
|
||||||
|
e.RunCommand("pulumi", "destroy", "--skip-preview", "--yes", "--non-interactive")
|
||||||
|
e.RunCommand("pulumi", "stack", "rm", "--yes")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue