core: Orphan addressing / targeting

Instead of trying to skip non-targeted orphans as they are added to
the graph in OrphanTransformer, remove knowledge of targeting from
OrphanTransformer and instead make the orphan resource nodes properly
addressable.

That allows us to use existing logic in TargetTransformer to filter out
the nodes appropriately. This does require adding TargetTransformer to the
list of transforms that run during DynamicExpand so that targeting can
be applied to nodes with expanded counts.

Fixes #4515
Fixes #2538
Fixes #4462
This commit is contained in:
Paul Hinze 2016-01-07 14:43:43 -06:00
parent 283d57c031
commit a0d3245ee3
11 changed files with 405 additions and 53 deletions

View File

@ -3359,6 +3359,60 @@ aws_instance.bar:
`)
}
// https://github.com/hashicorp/terraform/issues/4462
func TestContext2Apply_targetedDestroyModule(t *testing.T) {
m := testModule(t, "apply-targeted-module")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": resourceState("aws_instance", "i-bcd345"),
"aws_instance.bar": resourceState("aws_instance", "i-abc123"),
},
},
&ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*ResourceState{
"aws_instance.foo": resourceState("aws_instance", "i-bcd345"),
"aws_instance.bar": resourceState("aws_instance", "i-abc123"),
},
},
},
},
Targets: []string{"module.child.aws_instance.foo"},
Destroy: true,
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
checkStateString(t, state, `
aws_instance.bar:
ID = i-abc123
aws_instance.foo:
ID = i-bcd345
module.child:
aws_instance.bar:
ID = i-abc123
`)
}
func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) {
m := testModule(t, "apply-targeted-count")
p := testProvider("aws")

View File

@ -1647,6 +1647,12 @@ func TestContext2Plan_targetedOrphan(t *testing.T) {
ID: "i-789xyz",
},
},
"aws_instance.nottargeted": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "i-abc123",
},
},
},
},
},
@ -1667,8 +1673,150 @@ DESTROY: aws_instance.orphan
STATE:
aws_instance.nottargeted:
ID = i-abc123
aws_instance.orphan:
ID = i-789xyz`)
ID = i-789xyz
`)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}
// https://github.com/hashicorp/terraform/issues/2538
func TestContext2Plan_targetedModuleOrphan(t *testing.T) {
m := testModule(t, "plan-targeted-module-orphan")
p := testProvider("aws")
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: &State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*ResourceState{
"aws_instance.orphan": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "i-789xyz",
},
},
"aws_instance.nottargeted": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "i-abc123",
},
},
},
},
},
},
Destroy: true,
Targets: []string{"module.child.aws_instance.orphan"},
})
plan, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(`DIFF:
module.child:
DESTROY: aws_instance.orphan
STATE:
module.child:
aws_instance.nottargeted:
ID = i-abc123
aws_instance.orphan:
ID = i-789xyz
`)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}
// https://github.com/hashicorp/terraform/issues/4515
func TestContext2Plan_targetedOverTen(t *testing.T) {
m := testModule(t, "plan-targeted-over-ten")
p := testProvider("aws")
p.DiffFn = testDiffFn
resources := make(map[string]*ResourceState)
var expectedState []string
for i := 0; i < 13; i++ {
key := fmt.Sprintf("aws_instance.foo.%d", i)
id := fmt.Sprintf("i-abc%d", i)
resources[key] = &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{ID: id},
}
expectedState = append(expectedState,
fmt.Sprintf("%s:\n ID = %s\n", key, id))
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: resources,
},
},
},
Targets: []string{"aws_instance.foo[1]"},
})
plan, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(plan.String())
sort.Strings(expectedState)
expected := strings.TrimSpace(`
DIFF:
STATE:
aws_instance.foo.0:
ID = i-abc0
aws_instance.foo.1:
ID = i-abc1
aws_instance.foo.10:
ID = i-abc10
aws_instance.foo.11:
ID = i-abc11
aws_instance.foo.12:
ID = i-abc12
aws_instance.foo.2:
ID = i-abc2
aws_instance.foo.3:
ID = i-abc3
aws_instance.foo.4:
ID = i-abc4
aws_instance.foo.5:
ID = i-abc5
aws_instance.foo.6:
ID = i-abc6
aws_instance.foo.7:
ID = i-abc7
aws_instance.foo.8:
ID = i-abc8
aws_instance.foo.9:
ID = i-abc9
`)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}

View File

@ -163,9 +163,8 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
// expand orphans, which have all the same semantics in a destroy
// as a primary.
steps = append(steps, &OrphanTransformer{
State: state,
View: n.Resource.Id(),
Targets: n.Targets,
State: state,
View: n.Resource.Id(),
})
steps = append(steps, &DeposedTransformer{
@ -181,6 +180,12 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
})
}
// We always want to apply targeting
steps = append(steps, &TargetsTransformer{
ParsedTargets: n.Targets,
Destroy: n.DestroyMode != DestroyNone,
})
// Always end with the root being added
steps = append(steps, &RootTransformer{})

View File

@ -28,6 +28,15 @@ func TestParseResourceAddress(t *testing.T) {
Index: 2,
},
},
"implicit primary, explicit index over ten": {
Input: "aws_instance.foo[12]",
Expected: &ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 12,
},
},
"explicit primary, explicit index": {
Input: "aws_instance.foo.primary[2]",
Expected: &ResourceAddress{
@ -184,6 +193,21 @@ func TestResourceAddressEquals(t *testing.T) {
},
Expect: true,
},
"index over ten": {
Address: &ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 1,
},
Other: &ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 13,
},
Expect: false,
},
"different type": {
Address: &ResourceAddress{
Type: "aws_instance",

View File

@ -9,6 +9,7 @@ import (
"log"
"reflect"
"sort"
"strconv"
"strings"
"github.com/hashicorp/terraform/config"
@ -661,6 +662,65 @@ func (m *ModuleState) String() string {
return buf.String()
}
// ResourceStateKey is a structured representation of the key used for the
// ModuleState.Resources mapping
type ResourceStateKey struct {
Name string
Type string
Index int
}
// Equal determines whether two ResourceStateKeys are the same
func (rsk *ResourceStateKey) Equal(other *ResourceStateKey) bool {
if rsk == nil || other == nil {
return false
}
if rsk.Type != other.Type {
return false
}
if rsk.Name != other.Name {
return false
}
if rsk.Index != other.Index {
return false
}
return true
}
func (rsk *ResourceStateKey) String() string {
if rsk == nil {
return ""
}
if rsk.Index == -1 {
return fmt.Sprintf("%s.%s", rsk.Type, rsk.Name)
}
return fmt.Sprintf("%s.%s.%d", rsk.Type, rsk.Name, rsk.Index)
}
// ParseResourceStateKey accepts a key in the format used by
// ModuleState.Resources and returns a resource name and resource index. In the
// state, a resource has the format "type.name.index" or "type.name". In the
// latter case, the index is returned as -1.
func ParseResourceStateKey(k string) (*ResourceStateKey, error) {
parts := strings.Split(k, ".")
if len(parts) < 2 || len(parts) > 3 {
return nil, fmt.Errorf("Malformed resource state key: %s", k)
}
rsk := &ResourceStateKey{
Type: parts[0],
Name: parts[1],
Index: -1,
}
if len(parts) == 3 {
index, err := strconv.Atoi(parts[2])
if err != nil {
return nil, fmt.Errorf("Malformed resource state key index: %s", k)
}
rsk.Index = index
}
return rsk, nil
}
// ResourceState holds the state of a resource that is used so that
// a provider can find and manage an existing resource as well as for
// storing attributes that are used to populate variables of child

View File

@ -895,3 +895,57 @@ func TestUpgradeV1State(t *testing.T) {
t.Fatalf("bad: %#v", bt)
}
}
func TestParseResourceStateKey(t *testing.T) {
cases := []struct {
Input string
Expected *ResourceStateKey
ExpectedErr bool
}{
{
Input: "aws_instance.foo.3",
Expected: &ResourceStateKey{
Type: "aws_instance",
Name: "foo",
Index: 3,
},
},
{
Input: "aws_instance.foo.0",
Expected: &ResourceStateKey{
Type: "aws_instance",
Name: "foo",
Index: 0,
},
},
{
Input: "aws_instance.foo",
Expected: &ResourceStateKey{
Type: "aws_instance",
Name: "foo",
Index: -1,
},
},
{
Input: "aws_instance.foo.malformed",
ExpectedErr: true,
},
{
Input: "aws_instance.foo.malformedwithnumber.123",
ExpectedErr: true,
},
{
Input: "malformed",
ExpectedErr: true,
},
}
for _, tc := range cases {
rsk, err := ParseResourceStateKey(tc.Input)
if rsk != nil && tc.Expected != nil && !rsk.Equal(tc.Expected) {
t.Fatalf("%s: expected %s, got %s", tc.Input, tc.Expected, rsk)
}
if (err != nil) != tc.ExpectedErr {
t.Fatalf("%s: expected err: %t, got %s", tc.Input, tc.ExpectedErr, err)
}
}
}

View File

@ -0,0 +1,6 @@
# Once opon a time, there was a child module here
/*
module "child" {
source = "./child"
}
*/

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
count = 13
}

View File

@ -2,7 +2,6 @@ package terraform
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
@ -26,11 +25,6 @@ type OrphanTransformer struct {
// using the graph path.
Module *module.Tree
// Targets are user-specified resources to target. We need to be aware of
// these so we don't improperly identify orphans when they've just been
// filtered out of the graph via targeting.
Targets []ResourceAddress
// View, if non-nil will set a view on the module state.
View string
}
@ -68,22 +62,6 @@ func (t *OrphanTransformer) Transform(g *Graph) error {
}
resourceOrphans := state.Orphans(config)
if len(t.Targets) > 0 {
var targetedOrphans []string
for _, o := range resourceOrphans {
targeted := false
for _, t := range t.Targets {
prefix := fmt.Sprintf("%s.%s.%d", t.Type, t.Name, t.Index)
if strings.HasPrefix(o, prefix) {
targeted = true
}
}
if targeted {
targetedOrphans = append(targetedOrphans, o)
}
}
resourceOrphans = targetedOrphans
}
resourceVertexes = make([]dag.Vertex, len(resourceOrphans))
for i, k := range resourceOrphans {
@ -95,11 +73,15 @@ func (t *OrphanTransformer) Transform(g *Graph) error {
rs := state.Resources[k]
rsk, err := ParseResourceStateKey(k)
if err != nil {
return err
}
resourceVertexes[i] = g.Add(&graphNodeOrphanResource{
ResourceName: k,
ResourceType: rs.Type,
Provider: rs.Provider,
dependentOn: rs.Dependencies,
Path: g.Path,
ResourceKey: rsk,
Provider: rs.Provider,
dependentOn: rs.Dependencies,
})
}
}
@ -175,15 +157,25 @@ func (n *graphNodeOrphanModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error
// graphNodeOrphanResource is the graph vertex representing an orphan resource..
type graphNodeOrphanResource struct {
ResourceName string
ResourceType string
Provider string
Path []string
ResourceKey *ResourceStateKey
Provider string
dependentOn []string
}
func (n *graphNodeOrphanResource) ConfigType() GraphNodeConfigType {
return GraphNodeConfigTypeResource
}
func (n *graphNodeOrphanResource) ResourceAddress() *ResourceAddress {
return n.ResourceAddress()
return &ResourceAddress{
Index: n.ResourceKey.Index,
InstanceType: TypePrimary,
Name: n.ResourceKey.Name,
Path: n.Path[1:],
Type: n.ResourceKey.Type,
}
}
func (n *graphNodeOrphanResource) DependableName() []string {
@ -202,11 +194,11 @@ func (n *graphNodeOrphanResource) Flatten(p []string) (dag.Vertex, error) {
}
func (n *graphNodeOrphanResource) Name() string {
return fmt.Sprintf("%s (orphan)", n.ResourceName)
return fmt.Sprintf("%s (orphan)", n.ResourceKey)
}
func (n *graphNodeOrphanResource) ProvidedBy() []string {
return []string{resourceProvider(n.ResourceName, n.Provider)}
return []string{resourceProvider(n.ResourceKey.Type, n.Provider)}
}
// GraphNodeEvalable impl.
@ -217,7 +209,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}
// Build instance info
info := &InstanceInfo{Id: n.ResourceName, Type: n.ResourceType}
info := &InstanceInfo{Id: n.ResourceKey.String(), Type: n.ResourceKey.Type}
seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info})
// Refresh the resource
@ -230,7 +222,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
Output: &provider,
},
&EvalReadState{
Name: n.ResourceName,
Name: n.ResourceKey.String(),
Output: &state,
},
&EvalRefresh{
@ -240,8 +232,8 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
Output: &state,
},
&EvalWriteState{
Name: n.ResourceName,
ResourceType: n.ResourceType,
Name: n.ResourceKey.String(),
ResourceType: n.ResourceKey.Type,
Provider: n.Provider,
Dependencies: n.DependentOn(),
State: &state,
@ -257,7 +249,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalReadState{
Name: n.ResourceName,
Name: n.ResourceKey.String(),
Output: &state,
},
&EvalDiffDestroy{
@ -266,7 +258,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
Output: &diff,
},
&EvalWriteDiff{
Name: n.ResourceName,
Name: n.ResourceKey.String(),
Diff: &diff,
},
},
@ -280,7 +272,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalReadDiff{
Name: n.ResourceName,
Name: n.ResourceKey.String(),
Diff: &diff,
},
&EvalGetProvider{
@ -288,7 +280,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
Output: &provider,
},
&EvalReadState{
Name: n.ResourceName,
Name: n.ResourceKey.String(),
Output: &state,
},
&EvalApply{
@ -300,8 +292,8 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
Error: &err,
},
&EvalWriteState{
Name: n.ResourceName,
ResourceType: n.ResourceType,
Name: n.ResourceKey.String(),
ResourceType: n.ResourceKey.Type,
Provider: n.Provider,
Dependencies: n.DependentOn(),
State: &state,
@ -320,7 +312,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode {
}
func (n *graphNodeOrphanResource) dependableName() string {
return n.ResourceName
return n.ResourceKey.String()
}
// GraphNodeDestroyable impl.

View File

@ -333,17 +333,18 @@ func TestGraphNodeOrphanResource_impl(t *testing.T) {
var _ dag.Vertex = new(graphNodeOrphanResource)
var _ dag.NamedVertex = new(graphNodeOrphanResource)
var _ GraphNodeProviderConsumer = new(graphNodeOrphanResource)
var _ GraphNodeAddressable = new(graphNodeOrphanResource)
}
func TestGraphNodeOrphanResource_ProvidedBy(t *testing.T) {
n := &graphNodeOrphanResource{ResourceName: "aws_instance.foo"}
n := &graphNodeOrphanResource{ResourceKey: &ResourceStateKey{Type: "aws_instance"}}
if v := n.ProvidedBy(); v[0] != "aws" {
t.Fatalf("bad: %#v", v)
}
}
func TestGraphNodeOrphanResource_ProvidedBy_alias(t *testing.T) {
n := &graphNodeOrphanResource{ResourceName: "aws_instance.foo", Provider: "aws.bar"}
n := &graphNodeOrphanResource{ResourceKey: &ResourceStateKey{Type: "aws_instance"}, Provider: "aws.bar"}
if v := n.ProvidedBy(); v[0] != "aws.bar" {
t.Fatalf("bad: %#v", v)
}

View File

@ -13,20 +13,25 @@ type TargetsTransformer struct {
// List of targeted resource names specified by the user
Targets []string
// List of parsed targets, provided by callers like ResourceCountTransform
// that already have the targets parsed
ParsedTargets []ResourceAddress
// Set to true when we're in a `terraform destroy` or a
// `terraform plan -destroy`
Destroy bool
}
func (t *TargetsTransformer) Transform(g *Graph) error {
if len(t.Targets) > 0 {
// TODO: duplicated in OrphanTransformer; pull up parsing earlier
if len(t.Targets) > 0 && len(t.ParsedTargets) == 0 {
addrs, err := t.parseTargetAddresses()
if err != nil {
return err
}
targetedNodes, err := t.selectTargetedNodes(g, addrs)
t.ParsedTargets = addrs
}
if len(t.ParsedTargets) > 0 {
targetedNodes, err := t.selectTargetedNodes(g, t.ParsedTargets)
if err != nil {
return err
}