core: formalize resource addressing
Only used in targets for now. The plan is to use this for interpolation as well. This allows us to target: * individual resources expanded by `count` using bracket / index notation. * deposed / tainted resources with an `InstanceType` field after name Docs to follow.
This commit is contained in:
parent
40ebfb5ccc
commit
c6300d511c
|
@ -1535,6 +1535,98 @@ func TestContext2Refresh_targeted(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Refresh_targetedCount(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
m := testModule(t, "refresh-targeted-count")
|
||||||
|
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_vpc.metoo": resourceState("aws_vpc", "vpc-abc123"),
|
||||||
|
"aws_instance.notme": resourceState("aws_instance", "i-bcd345"),
|
||||||
|
"aws_instance.me.0": resourceState("aws_instance", "i-abc123"),
|
||||||
|
"aws_instance.me.1": resourceState("aws_instance", "i-cde567"),
|
||||||
|
"aws_instance.me.2": resourceState("aws_instance", "i-cde789"),
|
||||||
|
"aws_elb.meneither": resourceState("aws_elb", "lb-abc123"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Targets: []string{"aws_instance.me"},
|
||||||
|
})
|
||||||
|
|
||||||
|
refreshedResources := make([]string, 0, 2)
|
||||||
|
p.RefreshFn = func(i *InstanceInfo, is *InstanceState) (*InstanceState, error) {
|
||||||
|
refreshedResources = append(refreshedResources, i.Id)
|
||||||
|
return is, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ctx.Refresh()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target didn't specify index, so we should get all our instances
|
||||||
|
expected := []string{
|
||||||
|
"aws_vpc.metoo",
|
||||||
|
"aws_instance.me.0",
|
||||||
|
"aws_instance.me.1",
|
||||||
|
"aws_instance.me.2",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(refreshedResources, expected) {
|
||||||
|
t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Refresh_targetedCountIndex(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
m := testModule(t, "refresh-targeted-count")
|
||||||
|
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_vpc.metoo": resourceState("aws_vpc", "vpc-abc123"),
|
||||||
|
"aws_instance.notme": resourceState("aws_instance", "i-bcd345"),
|
||||||
|
"aws_instance.me.0": resourceState("aws_instance", "i-abc123"),
|
||||||
|
"aws_instance.me.1": resourceState("aws_instance", "i-cde567"),
|
||||||
|
"aws_instance.me.2": resourceState("aws_instance", "i-cde789"),
|
||||||
|
"aws_elb.meneither": resourceState("aws_elb", "lb-abc123"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Targets: []string{"aws_instance.me[0]"},
|
||||||
|
})
|
||||||
|
|
||||||
|
refreshedResources := make([]string, 0, 2)
|
||||||
|
p.RefreshFn = func(i *InstanceInfo, is *InstanceState) (*InstanceState, error) {
|
||||||
|
refreshedResources = append(refreshedResources, i.Id)
|
||||||
|
return is, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ctx.Refresh()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"aws_vpc.metoo", "aws_instance.me.0"}
|
||||||
|
if !reflect.DeepEqual(refreshedResources, expected) {
|
||||||
|
t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContext2Refresh_delete(t *testing.T) {
|
func TestContext2Refresh_delete(t *testing.T) {
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
m := testModule(t, "refresh-basic")
|
m := testModule(t, "refresh-basic")
|
||||||
|
@ -5274,6 +5366,66 @@ aws_instance.foo:
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Apply_targetedCount(t *testing.T) {
|
||||||
|
m := testModule(t, "apply-targeted-count")
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.ApplyFn = testApplyFn
|
||||||
|
p.DiffFn = testDiffFn
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Module: m,
|
||||||
|
Providers: map[string]ResourceProviderFactory{
|
||||||
|
"aws": testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
Targets: []string{"aws_instance.foo"},
|
||||||
|
})
|
||||||
|
|
||||||
|
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.foo.0:
|
||||||
|
ID = foo
|
||||||
|
aws_instance.foo.1:
|
||||||
|
ID = foo
|
||||||
|
aws_instance.foo.2:
|
||||||
|
ID = foo
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Apply_targetedCountIndex(t *testing.T) {
|
||||||
|
m := testModule(t, "apply-targeted-count")
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.ApplyFn = testApplyFn
|
||||||
|
p.DiffFn = testDiffFn
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Module: m,
|
||||||
|
Providers: map[string]ResourceProviderFactory{
|
||||||
|
"aws": testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
Targets: []string{"aws_instance.foo[1]"},
|
||||||
|
})
|
||||||
|
|
||||||
|
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.foo.1:
|
||||||
|
ID = foo
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestContext2Apply_targetedDestroy(t *testing.T) {
|
func TestContext2Apply_targetedDestroy(t *testing.T) {
|
||||||
m := testModule(t, "apply-targeted")
|
m := testModule(t, "apply-targeted")
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
|
@ -5319,6 +5471,59 @@ aws_instance.bar:
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) {
|
||||||
|
m := testModule(t, "apply-targeted-count")
|
||||||
|
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.0": resourceState("aws_instance", "i-bcd345"),
|
||||||
|
"aws_instance.foo.1": resourceState("aws_instance", "i-bcd345"),
|
||||||
|
"aws_instance.foo.2": resourceState("aws_instance", "i-bcd345"),
|
||||||
|
"aws_instance.bar.0": resourceState("aws_instance", "i-abc123"),
|
||||||
|
"aws_instance.bar.1": resourceState("aws_instance", "i-abc123"),
|
||||||
|
"aws_instance.bar.2": resourceState("aws_instance", "i-abc123"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Targets: []string{
|
||||||
|
"aws_instance.foo[2]",
|
||||||
|
"aws_instance.bar[1]",
|
||||||
|
},
|
||||||
|
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.0:
|
||||||
|
ID = i-abc123
|
||||||
|
aws_instance.bar.2:
|
||||||
|
ID = i-abc123
|
||||||
|
aws_instance.foo.0:
|
||||||
|
ID = i-bcd345
|
||||||
|
aws_instance.foo.1:
|
||||||
|
ID = i-bcd345
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestContext2Apply_unknownAttribute(t *testing.T) {
|
func TestContext2Apply_unknownAttribute(t *testing.T) {
|
||||||
m := testModule(t, "apply-unknown")
|
m := testModule(t, "apply-unknown")
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
|
|
|
@ -89,7 +89,11 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
|
||||||
return []GraphTransformer{
|
return []GraphTransformer{
|
||||||
// Create all our resources from the configuration and state
|
// Create all our resources from the configuration and state
|
||||||
&ConfigTransformer{Module: b.Root},
|
&ConfigTransformer{Module: b.Root},
|
||||||
&OrphanTransformer{State: b.State, Module: b.Root},
|
&OrphanTransformer{
|
||||||
|
State: b.State,
|
||||||
|
Module: b.Root,
|
||||||
|
Targeting: (len(b.Targets) > 0),
|
||||||
|
},
|
||||||
|
|
||||||
// Provider-related transformations
|
// Provider-related transformations
|
||||||
&MissingProviderTransformer{Providers: b.Providers},
|
&MissingProviderTransformer{Providers: b.Providers},
|
||||||
|
|
|
@ -21,6 +21,26 @@ type graphNodeConfig interface {
|
||||||
GraphNodeDependent
|
GraphNodeDependent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable is an interface that all graph nodes for the
|
||||||
|
// configuration graph need to implement in order to be be addressed / targeted
|
||||||
|
// properly.
|
||||||
|
type GraphNodeAddressable interface {
|
||||||
|
graphNodeConfig
|
||||||
|
|
||||||
|
ResourceAddress() *ResourceAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNodeTargetable is an interface for graph nodes to implement when they
|
||||||
|
// need to be told about incoming targets. This is useful for nodes that need
|
||||||
|
// to respect targets as they dynamically expand. Note that the list of targets
|
||||||
|
// provided will contain every target provided, and each implementing graph
|
||||||
|
// node must filter this list to targets considered relevant.
|
||||||
|
type GraphNodeTargetable interface {
|
||||||
|
GraphNodeAddressable
|
||||||
|
|
||||||
|
SetTargets([]ResourceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeConfigModule represents a module within the configuration graph.
|
// GraphNodeConfigModule represents a module within the configuration graph.
|
||||||
type GraphNodeConfigModule struct {
|
type GraphNodeConfigModule struct {
|
||||||
Path []string
|
Path []string
|
||||||
|
@ -191,6 +211,9 @@ type GraphNodeConfigResource struct {
|
||||||
// If this is set to anything other than destroyModeNone, then this
|
// If this is set to anything other than destroyModeNone, then this
|
||||||
// resource represents a resource that will be destroyed in some way.
|
// resource represents a resource that will be destroyed in some way.
|
||||||
DestroyMode GraphNodeDestroyMode
|
DestroyMode GraphNodeDestroyMode
|
||||||
|
|
||||||
|
// Used during DynamicExpand to target indexes
|
||||||
|
Targets []ResourceAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *GraphNodeConfigResource) DependableName() []string {
|
func (n *GraphNodeConfigResource) DependableName() []string {
|
||||||
|
@ -279,6 +302,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
steps = append(steps, &ResourceCountTransformer{
|
steps = append(steps, &ResourceCountTransformer{
|
||||||
Resource: n.Resource,
|
Resource: n.Resource,
|
||||||
Destroy: n.DestroyMode != DestroyNone,
|
Destroy: n.DestroyMode != DestroyNone,
|
||||||
|
Targets: n.Targets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,8 +313,9 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
// expand orphans, which have all the same semantics in a destroy
|
// expand orphans, which have all the same semantics in a destroy
|
||||||
// as a primary.
|
// as a primary.
|
||||||
steps = append(steps, &OrphanTransformer{
|
steps = append(steps, &OrphanTransformer{
|
||||||
State: state,
|
State: state,
|
||||||
View: n.Resource.Id(),
|
View: n.Resource.Id(),
|
||||||
|
Targeting: (len(n.Targets) > 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
steps = append(steps, &DeposedTransformer{
|
steps = append(steps, &DeposedTransformer{
|
||||||
|
@ -314,6 +339,22 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||||
return b.Build(ctx.Path())
|
return b.Build(ctx.Path())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable impl.
|
||||||
|
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
|
||||||
|
return &ResourceAddress{
|
||||||
|
// Indicates no specific index; will match on other three fields
|
||||||
|
Index: -1,
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Name: n.Resource.Name,
|
||||||
|
Type: n.Resource.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNodeTargetable impl.
|
||||||
|
func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) {
|
||||||
|
n.Targets = targets
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeEvalable impl.
|
// GraphNodeEvalable impl.
|
||||||
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
||||||
return &EvalSequence{
|
return &EvalSequence{
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
//go:generate stringer -type=InstanceType instancetype.go
|
||||||
|
|
||||||
|
// InstanceType is an enum of the various types of instances store in the State
|
||||||
|
type InstanceType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeInvalid InstanceType = iota
|
||||||
|
TypePrimary
|
||||||
|
TypeTainted
|
||||||
|
TypeDeposed
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
// generated by stringer -type=InstanceType instancetype.go; DO NOT EDIT
|
||||||
|
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed"
|
||||||
|
|
||||||
|
var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44}
|
||||||
|
|
||||||
|
func (i InstanceType) String() string {
|
||||||
|
if i < 0 || i+1 >= InstanceType(len(_InstanceType_index)) {
|
||||||
|
return fmt.Sprintf("InstanceType(%d)", i)
|
||||||
|
}
|
||||||
|
return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]]
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceAddress is a way of identifying an individual resource (or,
|
||||||
|
// eventually, a subset of resources) within the state. It is used for Targets.
|
||||||
|
type ResourceAddress struct {
|
||||||
|
Index int
|
||||||
|
InstanceType InstanceType
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseResourceAddress(s string) (*ResourceAddress, error) {
|
||||||
|
matches, err := tokenizeResourceAddress(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resourceIndex := -1
|
||||||
|
if matches["index"] != "" {
|
||||||
|
var err error
|
||||||
|
if resourceIndex, err = strconv.Atoi(matches["index"]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instanceType := TypePrimary
|
||||||
|
if matches["instance_type"] != "" {
|
||||||
|
var err error
|
||||||
|
if instanceType, err = ParseInstanceType(matches["instance_type"]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ResourceAddress{
|
||||||
|
Index: resourceIndex,
|
||||||
|
InstanceType: instanceType,
|
||||||
|
Name: matches["name"],
|
||||||
|
Type: matches["type"],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (addr *ResourceAddress) Equals(raw interface{}) bool {
|
||||||
|
other, ok := raw.(*ResourceAddress)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
indexMatch := (addr.Index == -1 ||
|
||||||
|
other.Index == -1 ||
|
||||||
|
addr.Index == other.Index)
|
||||||
|
|
||||||
|
return (indexMatch &&
|
||||||
|
addr.InstanceType == other.InstanceType &&
|
||||||
|
addr.Name == other.Name &&
|
||||||
|
addr.Type == other.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseInstanceType(s string) (InstanceType, error) {
|
||||||
|
switch s {
|
||||||
|
case "primary":
|
||||||
|
return TypePrimary, nil
|
||||||
|
case "deposed":
|
||||||
|
return TypeDeposed, nil
|
||||||
|
case "tainted":
|
||||||
|
return TypeTainted, nil
|
||||||
|
default:
|
||||||
|
return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenizeResourceAddress(s string) (map[string]string, error) {
|
||||||
|
// Example of portions of the regexp below using the
|
||||||
|
// string "aws_instance.web.tainted[1]"
|
||||||
|
re := regexp.MustCompile(`\A` +
|
||||||
|
// "aws_instance"
|
||||||
|
`(?P<type>\w+)\.` +
|
||||||
|
// "web"
|
||||||
|
`(?P<name>\w+)` +
|
||||||
|
// "tainted" (optional, omission implies: "primary")
|
||||||
|
`(?:\.(?P<instance_type>\w+))?` +
|
||||||
|
// "1" (optional, omission implies: "0")
|
||||||
|
`(?:\[(?P<index>\d+)\])?` +
|
||||||
|
`\z`)
|
||||||
|
groupNames := re.SubexpNames()
|
||||||
|
rawMatches := re.FindAllStringSubmatch(s, -1)
|
||||||
|
if len(rawMatches) != 1 {
|
||||||
|
return nil, fmt.Errorf("Problem parsing address: %q", s)
|
||||||
|
}
|
||||||
|
matches := make(map[string]string)
|
||||||
|
for i, m := range rawMatches[0] {
|
||||||
|
matches[groupNames[i]] = m
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseResourceAddress(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Input string
|
||||||
|
Expected *ResourceAddress
|
||||||
|
}{
|
||||||
|
"implicit primary, no specific index": {
|
||||||
|
Input: "aws_instance.foo",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"implicit primary, explicit index": {
|
||||||
|
Input: "aws_instance.foo[2]",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"explicit primary, explicit index": {
|
||||||
|
Input: "aws_instance.foo.primary[2]",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tainted": {
|
||||||
|
Input: "aws_instance.foo.tainted",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeTainted,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"deposed": {
|
||||||
|
Input: "aws_instance.foo.deposed",
|
||||||
|
Expected: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeDeposed,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
out, err := ParseResourceAddress(tc.Input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(out, tc.Expected) {
|
||||||
|
t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceAddressEquals(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Address *ResourceAddress
|
||||||
|
Other interface{}
|
||||||
|
Expect bool
|
||||||
|
}{
|
||||||
|
"basic match": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"address does not set index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 3,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"other does not set index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 3,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"neither sets index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: -1,
|
||||||
|
},
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
"different type": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_vpc",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different name": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "bar",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different instance type": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypeTainted,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
"different index": {
|
||||||
|
Address: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Other: &ResourceAddress{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Name: "foo",
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
actual := tc.Address.Equals(tc.Other)
|
||||||
|
if actual != tc.Expect {
|
||||||
|
t.Fatalf("%q: expected equals: %t, got %t for:\n%#v\n%#v",
|
||||||
|
tn, tc.Expect, actual, tc.Address, tc.Other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
count = 3
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
resource "aws_vpc" "metoo" {}
|
||||||
|
resource "aws_instance" "notme" { }
|
||||||
|
resource "aws_instance" "me" {
|
||||||
|
vpc_id = "${aws_vpc.metoo.id}"
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
resource "aws_elb" "meneither" {
|
||||||
|
instances = ["${aws_instance.me.*.id}"]
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
@ -25,6 +26,11 @@ type OrphanTransformer struct {
|
||||||
// using the graph path.
|
// using the graph path.
|
||||||
Module *module.Tree
|
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.
|
||||||
|
Targeting bool
|
||||||
|
|
||||||
// View, if non-nil will set a view on the module state.
|
// View, if non-nil will set a view on the module state.
|
||||||
View string
|
View string
|
||||||
}
|
}
|
||||||
|
@ -35,6 +41,13 @@ func (t *OrphanTransformer) Transform(g *Graph) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Targeting {
|
||||||
|
log.Printf("Skipping orphan transformer because we have targets.")
|
||||||
|
// If we are in a run where we are targeting nodes, we won't process
|
||||||
|
// orphans for this run.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Build up all our state representatives
|
// Build up all our state representatives
|
||||||
resourceRep := make(map[string]struct{})
|
resourceRep := make(map[string]struct{})
|
||||||
for _, v := range g.Vertices() {
|
for _, v := range g.Vertices() {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
type ResourceCountTransformer struct {
|
type ResourceCountTransformer struct {
|
||||||
Resource *config.Resource
|
Resource *config.Resource
|
||||||
Destroy bool
|
Destroy bool
|
||||||
|
Targets []ResourceAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
|
@ -27,7 +28,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each count, build and add the node
|
// For each count, build and add the node
|
||||||
nodes := make([]dag.Vertex, count)
|
nodes := make([]dag.Vertex, 0, count)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
// Set the index. If our count is 1 we special case it so that
|
// Set the index. If our count is 1 we special case it so that
|
||||||
// we handle the "resource.0" and "resource" boundary properly.
|
// we handle the "resource.0" and "resource" boundary properly.
|
||||||
|
@ -49,9 +50,14 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip nodes if targeting excludes them
|
||||||
|
if !t.nodeIsTargeted(node) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Add the node now
|
// Add the node now
|
||||||
nodes[i] = node
|
nodes = append(nodes, node)
|
||||||
g.Add(nodes[i])
|
g.Add(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the dependency connections
|
// Make the dependency connections
|
||||||
|
@ -64,6 +70,25 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool {
|
||||||
|
// no targets specified, everything stays in the graph
|
||||||
|
if len(t.Targets) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
addressable, ok := node.(GraphNodeAddressable)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := addressable.ResourceAddress()
|
||||||
|
for _, targetAddr := range t.Targets {
|
||||||
|
if targetAddr.Equals(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type graphNodeExpandedResource struct {
|
type graphNodeExpandedResource struct {
|
||||||
Index int
|
Index int
|
||||||
Resource *config.Resource
|
Resource *config.Resource
|
||||||
|
@ -77,6 +102,23 @@ func (n *graphNodeExpandedResource) Name() string {
|
||||||
return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index)
|
return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeAddressable impl.
|
||||||
|
func (n *graphNodeExpandedResource) ResourceAddress() *ResourceAddress {
|
||||||
|
// We want this to report the logical index properly, so we must undo the
|
||||||
|
// special case from the expand
|
||||||
|
index := n.Index
|
||||||
|
if index == -1 {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
return &ResourceAddress{
|
||||||
|
Index: index,
|
||||||
|
// TODO: kjkjkj
|
||||||
|
InstanceType: TypePrimary,
|
||||||
|
Name: n.Resource.Name,
|
||||||
|
Type: n.Resource.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GraphNodeDependable impl.
|
// GraphNodeDependable impl.
|
||||||
func (n *graphNodeExpandedResource) DependableName() []string {
|
func (n *graphNodeExpandedResource) DependableName() []string {
|
||||||
return []string{
|
return []string{
|
||||||
|
|
|
@ -16,13 +16,20 @@ type TargetsTransformer struct {
|
||||||
|
|
||||||
func (t *TargetsTransformer) Transform(g *Graph) error {
|
func (t *TargetsTransformer) Transform(g *Graph) error {
|
||||||
if len(t.Targets) > 0 {
|
if len(t.Targets) > 0 {
|
||||||
targetedNodes, err := t.selectTargetedNodes(g)
|
// TODO: duplicated in OrphanTransformer; pull up parsing earlier
|
||||||
|
addrs, err := t.parseTargetAddresses()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetedNodes, err := t.selectTargetedNodes(g, addrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range g.Vertices() {
|
for _, v := range g.Vertices() {
|
||||||
if !targetedNodes.Include(v) {
|
if targetedNodes.Include(v) {
|
||||||
|
} else {
|
||||||
g.Remove(v)
|
g.Remove(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +37,20 @@ func (t *TargetsTransformer) Transform(g *Graph) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) {
|
func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, error) {
|
||||||
|
addrs := make([]ResourceAddress, len(t.Targets))
|
||||||
|
for i, target := range t.Targets {
|
||||||
|
ta, err := ParseResourceAddress(target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addrs[i] = *ta
|
||||||
|
}
|
||||||
|
return addrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetsTransformer) selectTargetedNodes(
|
||||||
|
g *Graph, addrs []ResourceAddress) (*dag.Set, error) {
|
||||||
targetedNodes := new(dag.Set)
|
targetedNodes := new(dag.Set)
|
||||||
for _, v := range g.Vertices() {
|
for _, v := range g.Vertices() {
|
||||||
// Keep all providers; they'll be pruned later if necessary
|
// Keep all providers; they'll be pruned later if necessary
|
||||||
|
@ -39,14 +59,18 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the remaining filter, we only care about Resources and their deps
|
// For the remaining filter, we only care about addressable nodes
|
||||||
r, ok := v.(*GraphNodeConfigResource)
|
r, ok := v.(GraphNodeAddressable)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.resourceIsTarget(r) {
|
if t.nodeIsTarget(r, addrs) {
|
||||||
targetedNodes.Add(r)
|
targetedNodes.Add(r)
|
||||||
|
// If the node would like to know about targets, tell it.
|
||||||
|
if n, ok := r.(GraphNodeTargetable); ok {
|
||||||
|
n.SetTargets(addrs)
|
||||||
|
}
|
||||||
|
|
||||||
var deps *dag.Set
|
var deps *dag.Set
|
||||||
var err error
|
var err error
|
||||||
|
@ -67,9 +91,11 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) {
|
||||||
return targetedNodes, nil
|
return targetedNodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TargetsTransformer) resourceIsTarget(r *GraphNodeConfigResource) bool {
|
func (t *TargetsTransformer) nodeIsTarget(
|
||||||
for _, target := range t.Targets {
|
r GraphNodeAddressable, addrs []ResourceAddress) bool {
|
||||||
if target == r.Name() {
|
addr := r.ResourceAddress()
|
||||||
|
for _, targetAddr := range addrs {
|
||||||
|
if targetAddr.Equals(addr) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue