Skip to content

Commit

Permalink
core: Orphan addressing / targeting
Browse files Browse the repository at this point in the history
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
  • Loading branch information
phinze committed Jan 19, 2016
1 parent 283d57c commit a0d3245
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 53 deletions.
54 changes: 54 additions & 0 deletions terraform/context_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
150 changes: 149 additions & 1 deletion terraform/context_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
},
},
},
Expand All @@ -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)
}
Expand Down
11 changes: 8 additions & 3 deletions terraform/graph_config_node_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{})

Expand Down
24 changes: 24 additions & 0 deletions terraform/resource_address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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",
Expand Down
60 changes: 60 additions & 0 deletions terraform/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"reflect"
"sort"
"strconv"
"strings"

"github.com/hashicorp/terraform/config"
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit a0d3245

Please sign in to comment.