0a38bc295c
* Fix issue with --target deletion dependant calculation The code that computed --target deletion dependants was not correct. It used parent/child component relationships, but did not respect actual DAG dependencies. As a result, it could erroneously leave hanging references to resources that no longer exist after performing a `pulumi destroy --target X` operation. This manifested in bugs like https://github.com/pulumi/pulumi/issues/6283, which is fixed by this change. The solution is to compute the (transitive!) dependency graph correctly, factoring in both parent/child, as well as explicit and implicit, dependencies. The existing logic does the correct thing once we do this. I've also added tests for this area, including regression tests that cover transitive dependency relationships, as well as ones that would cause an infinite loop given a naive implementation. * Add a changelog entry * Fix failing test to include all destroyed targets Unless I'm missing something, the entire tree should be deleted in this test case because A is the ancestor for the entire tree. * Use DependencyGraph to compute dependents Per code review feedback from @pgavlin. Co-authored-by: Anton Tayanovskyy <anton@pulumi.com>
146 lines
5.3 KiB
Go
146 lines
5.3 KiB
Go
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
|
|
|
package graph
|
|
|
|
import (
|
|
"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/util/contract"
|
|
)
|
|
|
|
// DependencyGraph represents a dependency graph encoded within a resource snapshot.
|
|
type DependencyGraph struct {
|
|
index map[*resource.State]int // A mapping of resource pointers to indexes within the snapshot
|
|
resources []*resource.State // The list of resources, obtained from the snapshot
|
|
childrenOf map[resource.URN][]int // Pre-computed map of transitive children for each resource
|
|
}
|
|
|
|
// DependingOn returns a slice containing all resources that directly or indirectly
|
|
// depend upon the given resource. The returned slice is guaranteed to be in topological
|
|
// order with respect to the snapshot dependency graph.
|
|
//
|
|
// 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, includeChildren bool) []*resource.State {
|
|
// This implementation relies on the detail that snapshots are stored in a valid
|
|
// topological order.
|
|
var dependents []*resource.State
|
|
dependentSet := make(map[resource.URN]bool)
|
|
|
|
cursorIndex, ok := dg.index[res]
|
|
contract.Assert(ok)
|
|
dependentSet[res.URN] = true
|
|
|
|
isDependent := func(candidate *resource.State) bool {
|
|
if ignore[candidate.URN] {
|
|
return false
|
|
}
|
|
if includeChildren && candidate.Parent == res.URN {
|
|
return true
|
|
}
|
|
for _, dependency := range candidate.Dependencies {
|
|
if dependentSet[dependency] {
|
|
return true
|
|
}
|
|
}
|
|
if candidate.Provider != "" {
|
|
ref, err := providers.ParseReference(candidate.Provider)
|
|
contract.Assert(err == nil)
|
|
if dependentSet[ref.URN()] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// The dependency graph encoded directly within the snapshot is the reverse of
|
|
// the graph that we actually want to operate upon. Edges in the snapshot graph
|
|
// originate in a resource and go to that resource's dependencies.
|
|
//
|
|
// The `DependingOn` is simpler when operating on the reverse of the snapshot graph,
|
|
// where edges originate in a resource and go to resources that depend on that resource.
|
|
// In this graph, `DependingOn` for a resource is the set of resources that are reachable from the
|
|
// given resource.
|
|
//
|
|
// To accomplish this without building up an entire graph data structure, we'll do a linear
|
|
// scan of the resource list starting at the requested resource and ending at the end of
|
|
// the list. All resources that depend directly or indirectly on `res` are prepended
|
|
// onto `dependents`.
|
|
for i := cursorIndex + 1; i < len(dg.resources); i++ {
|
|
candidate := dg.resources[i]
|
|
if isDependent(candidate) {
|
|
dependents = append(dependents, candidate)
|
|
dependentSet[candidate.URN] = true
|
|
}
|
|
}
|
|
|
|
return dependents
|
|
}
|
|
|
|
// DependenciesOf returns a ResourceSet of resources upon which the given resource depends. The resource's parent is
|
|
// included in the returned set.
|
|
func (dg *DependencyGraph) DependenciesOf(res *resource.State) ResourceSet {
|
|
set := make(ResourceSet)
|
|
|
|
dependentUrns := make(map[resource.URN]bool)
|
|
for _, dep := range res.Dependencies {
|
|
dependentUrns[dep] = true
|
|
}
|
|
|
|
if res.Provider != "" {
|
|
ref, err := providers.ParseReference(res.Provider)
|
|
contract.Assert(err == nil)
|
|
dependentUrns[ref.URN()] = true
|
|
}
|
|
|
|
cursorIndex, ok := dg.index[res]
|
|
contract.Assert(ok)
|
|
for i := cursorIndex - 1; i >= 0; i-- {
|
|
candidate := dg.resources[i]
|
|
// Include all resources that are dependencies of the resource
|
|
if dependentUrns[candidate.URN] {
|
|
set[candidate] = true
|
|
// If the dependency is a component, all transitive children of the dependency that are before this
|
|
// resource in the topological sort are also implicitly dependencies. This is necessary because for remote
|
|
// components, the dependencies will not include the transitive set of children directly, but will include
|
|
// the parent component. We must walk that component's children here to ensure they are treated as
|
|
// dependencies. Transitive children of the dependency that are after the resource in the topological sort
|
|
// are not included as this could lead to cycles in the dependency order.
|
|
if !candidate.Custom {
|
|
for _, transitiveCandidateIndex := range dg.childrenOf[candidate.URN] {
|
|
if transitiveCandidateIndex < cursorIndex {
|
|
set[dg.resources[transitiveCandidateIndex]] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Include the resource's parent, as the resource depends on it's parent existing.
|
|
if candidate.URN == res.Parent {
|
|
set[candidate] = true
|
|
}
|
|
}
|
|
|
|
return set
|
|
}
|
|
|
|
// NewDependencyGraph creates a new DependencyGraph from a list of resources.
|
|
// The resources should be in topological order with respect to their dependencies, including
|
|
// parents appearing before children.
|
|
func NewDependencyGraph(resources []*resource.State) *DependencyGraph {
|
|
index := make(map[*resource.State]int)
|
|
childrenOf := make(map[resource.URN][]int)
|
|
|
|
urnIndex := make(map[resource.URN]int)
|
|
for idx, res := range resources {
|
|
index[res] = idx
|
|
urnIndex[res.URN] = idx
|
|
parent := res.Parent
|
|
for parent != "" {
|
|
childrenOf[parent] = append(childrenOf[parent], idx)
|
|
parent = resources[urnIndex[parent]].Parent
|
|
}
|
|
}
|
|
|
|
return &DependencyGraph{index, resources, childrenOf}
|
|
}
|