Skip to content

Commit

Permalink
Merge pull request #4574 from hashicorp/phinze/orphan-addressing
Browse files Browse the repository at this point in the history
Orphan addressing / targeting
  • Loading branch information
phinze committed Jan 21, 2016
2 parents 608aa69 + a0d3245 commit 8d8b287
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 @@ -1670,6 +1670,12 @@ func TestContext2Plan_targetedOrphan(t *testing.T) {
ID: "i-789xyz",
},
},
"aws_instance.nottargeted": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "i-abc123",
},
},
},
},
},
Expand All @@ -1690,8 +1696,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 8d8b287

Please sign in to comment.