core: module targeting

Adds the ability to target resources within modules, like:

module.mymod.aws_instance.foo

And the ability to target all resources inside a module, like:

module.mymod

Closes #1434
This commit is contained in:
Paul Hinze 2015-05-01 11:01:49 -05:00
parent cebcee5c63
commit 5d50264c31
12 changed files with 303 additions and 32 deletions

View File

@ -6128,6 +6128,87 @@ aws_instance.foo.1:
`) `)
} }
func TestContext2Apply_targetedModule(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),
},
Targets: []string{"module.child"},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
mod := state.ModuleByPath([]string{"root", "child"})
if mod == nil {
t.Fatalf("no child module found in the state!\n\n%#v", state)
}
if len(mod.Resources) != 2 {
t.Fatalf("expected 2 resources, got: %#v", mod.Resources)
}
checkStateString(t, state, `
<no state>
module.child:
aws_instance.bar:
ID = foo
num = 2
type = aws_instance
aws_instance.foo:
ID = foo
num = 2
type = aws_instance
`)
}
func TestContext2Apply_targetedModuleResource(t *testing.T) {
m := testModule(t, "apply-targeted-module-resource")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Targets: []string{"module.child.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)
}
mod := state.ModuleByPath([]string{"root", "child"})
if len(mod.Resources) != 1 {
t.Fatalf("expected 1 resource, got: %#v", mod.Resources)
}
checkStateString(t, state, `
<no state>
module.child:
aws_instance.foo:
ID = foo
num = 2
type = aws_instance
`)
}
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")

View File

@ -19,6 +19,8 @@ type GraphNodeConfigResource struct {
// Used during DynamicExpand to target indexes // Used during DynamicExpand to target indexes
Targets []ResourceAddress Targets []ResourceAddress
Path []string
} }
func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType { func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType {
@ -174,7 +176,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
// GraphNodeAddressable impl. // GraphNodeAddressable impl.
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress { func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
return &ResourceAddress{ return &ResourceAddress{
// Indicates no specific index; will match on other three fields Path: n.Path[1:],
Index: -1, Index: -1,
InstanceType: TypePrimary, InstanceType: TypePrimary,
Name: n.Resource.Name, Name: n.Resource.Name,

View File

@ -2,14 +2,22 @@ package terraform
import ( import (
"fmt" "fmt"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"strings"
) )
// ResourceAddress is a way of identifying an individual resource (or, // ResourceAddress is a way of identifying an individual resource (or,
// eventually, a subset of resources) within the state. It is used for Targets. // eventually, a subset of resources) within the state. It is used for Targets.
type ResourceAddress struct { type ResourceAddress struct {
// Addresses a resource falling somewhere in the module path
// When specified alone, addresses all resources within a module path
Path []string
// Addresses a specific resource that occurs in a list
Index int Index int
InstanceType InstanceType InstanceType InstanceType
Name string Name string
Type string Type string
@ -20,22 +28,18 @@ func ParseResourceAddress(s string) (*ResourceAddress, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
resourceIndex := -1 resourceIndex, err := ParseResourceIndex(matches["index"])
if matches["index"] != "" { if err != nil {
var err error
if resourceIndex, err = strconv.Atoi(matches["index"]); err != nil {
return nil, err return nil, err
} }
} instanceType, err := ParseInstanceType(matches["instance_type"])
instanceType := TypePrimary if err != nil {
if matches["instance_type"] != "" {
var err error
if instanceType, err = ParseInstanceType(matches["instance_type"]); err != nil {
return nil, err return nil, err
} }
} path := ParseResourcePath(matches["path"])
return &ResourceAddress{ return &ResourceAddress{
Path: path,
Index: resourceIndex, Index: resourceIndex,
InstanceType: instanceType, InstanceType: instanceType,
Name: matches["name"], Name: matches["name"],
@ -49,19 +53,55 @@ func (addr *ResourceAddress) Equals(raw interface{}) bool {
return false return false
} }
pathMatch := ((len(addr.Path) == 0 && len(other.Path) == 0) ||
reflect.DeepEqual(addr.Path, other.Path))
indexMatch := (addr.Index == -1 || indexMatch := (addr.Index == -1 ||
other.Index == -1 || other.Index == -1 ||
addr.Index == other.Index) addr.Index == other.Index)
return (indexMatch && nameMatch := (addr.Name == "" ||
addr.InstanceType == other.InstanceType && other.Name == "" ||
addr.Name == other.Name && addr.Name == other.Name)
typeMatch := (addr.Type == "" ||
other.Type == "" ||
addr.Type == other.Type) addr.Type == other.Type)
return (pathMatch &&
indexMatch &&
addr.InstanceType == other.InstanceType &&
nameMatch &&
typeMatch)
}
func ParseResourceIndex(s string) (int, error) {
if s == "" {
return -1, nil
}
return strconv.Atoi(s)
}
func ParseResourcePath(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, ".")
path := make([]string, 0, len(parts))
for _, s := range parts {
// Due to the limitations of the regexp match below, the path match has
// some noise in it we have to filter out :|
if s == "" || s == "module" {
continue
}
path = append(path, s)
}
return path
} }
func ParseInstanceType(s string) (InstanceType, error) { func ParseInstanceType(s string) (InstanceType, error) {
switch s { switch s {
case "primary": case "", "primary":
return TypePrimary, nil return TypePrimary, nil
case "deposed": case "deposed":
return TypeDeposed, nil return TypeDeposed, nil
@ -76,10 +116,10 @@ func tokenizeResourceAddress(s string) (map[string]string, error) {
// Example of portions of the regexp below using the // Example of portions of the regexp below using the
// string "aws_instance.web.tainted[1]" // string "aws_instance.web.tainted[1]"
re := regexp.MustCompile(`\A` + re := regexp.MustCompile(`\A` +
// "aws_instance" // "module.foo.module.bar" (optional)
`(?P<type>[^.]+)\.` + `(?P<path>(?:module\.[^.]+\.?)*)` +
// "web" // "aws_instance.web" (optional when module path specified)
`(?P<name>[^.[]+)` + `(?:(?P<type>[^.]+)\.(?P<name>[^.[]+))?` +
// "tainted" (optional, omission implies: "primary") // "tainted" (optional, omission implies: "primary")
`(?:\.(?P<instance_type>\w+))?` + `(?:\.(?P<instance_type>\w+))?` +
// "1" (optional, omission implies: "0") // "1" (optional, omission implies: "0")

View File

@ -64,6 +64,46 @@ func TestParseResourceAddress(t *testing.T) {
Index: -1, Index: -1,
}, },
}, },
"in a module": {
Input: "module.child.aws_instance.foo",
Expected: &ResourceAddress{
Path: []string{"child"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
},
"nested modules": {
Input: "module.a.module.b.module.forever.aws_instance.foo",
Expected: &ResourceAddress{
Path: []string{"a", "b", "forever"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
},
"just a module": {
Input: "module.a",
Expected: &ResourceAddress{
Path: []string{"a"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
},
"just a nested module": {
Input: "module.a.module.b",
Expected: &ResourceAddress{
Path: []string{"a", "b"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
},
} }
for tn, tc := range cases { for tn, tc := range cases {
@ -204,6 +244,57 @@ func TestResourceAddressEquals(t *testing.T) {
}, },
Expect: false, Expect: false,
}, },
"module address matches address of resource inside module": {
Address: &ResourceAddress{
Path: []string{"a", "b"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
Other: &ResourceAddress{
Path: []string{"a", "b"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 0,
},
Expect: true,
},
"module address doesn't match resource outside module": {
Address: &ResourceAddress{
Path: []string{"a", "b"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
Other: &ResourceAddress{
Path: []string{"a"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 0,
},
Expect: false,
},
"nil path vs empty path should match": {
Address: &ResourceAddress{
Path: []string{},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
Other: &ResourceAddress{
Path: nil,
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 0,
},
Expect: true,
},
} }
for tn, tc := range cases { for tn, tc := range cases {

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
num = "2"
}

View File

@ -0,0 +1,7 @@
module "child" {
source = "./child"
}
resource "aws_instance" "bar" {
foo = "bar"
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
num = "2"
}

View File

@ -0,0 +1,11 @@
module "child" {
source = "./child"
}
resource "aws_instance" "foo" {
foo = "bar"
}
resource "aws_instance" "bar" {
foo = "bar"
}

View File

@ -55,7 +55,10 @@ func (t *ConfigTransformer) Transform(g *Graph) error {
// Write all the resources out // Write all the resources out
for _, r := range config.Resources { for _, r := range config.Resources {
nodes = append(nodes, &GraphNodeConfigResource{Resource: r}) nodes = append(nodes, &GraphNodeConfigResource{
Resource: r,
Path: g.Path,
})
} }
// Write all the modules out // Write all the modules out

View File

@ -44,6 +44,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
var node dag.Vertex = &graphNodeExpandedResource{ var node dag.Vertex = &graphNodeExpandedResource{
Index: index, Index: index,
Resource: t.Resource, Resource: t.Resource,
Path: g.Path,
} }
if t.Destroy { if t.Destroy {
node = &graphNodeExpandedResourceDestroy{ node = &graphNodeExpandedResourceDestroy{
@ -93,6 +94,7 @@ func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool {
type graphNodeExpandedResource struct { type graphNodeExpandedResource struct {
Index int Index int
Resource *config.Resource Resource *config.Resource
Path []string
} }
func (n *graphNodeExpandedResource) Name() string { func (n *graphNodeExpandedResource) Name() string {
@ -112,8 +114,8 @@ func (n *graphNodeExpandedResource) ResourceAddress() *ResourceAddress {
index = 0 index = 0
} }
return &ResourceAddress{ return &ResourceAddress{
Path: n.Path[1:],
Index: index, Index: index,
// TODO: kjkjkj
InstanceType: TypePrimary, InstanceType: TypePrimary,
Name: n.Resource.Name, Name: n.Resource.Name,
Type: n.Resource.Type, Type: n.Resource.Type,

View File

@ -1,6 +1,10 @@
package terraform package terraform
import "github.com/hashicorp/terraform/dag" import (
"log"
"github.com/hashicorp/terraform/dag"
)
// TargetsTransformer is a GraphTransformer that, when the user specifies a // TargetsTransformer is a GraphTransformer that, when the user specifies a
// list of resources to target, limits the graph to only those resources and // list of resources to target, limits the graph to only those resources and
@ -30,6 +34,7 @@ func (t *TargetsTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() { for _, v := range g.Vertices() {
if _, ok := v.(GraphNodeAddressable); ok { if _, ok := v.(GraphNodeAddressable); ok {
if !targetedNodes.Include(v) { if !targetedNodes.Include(v) {
log.Printf("[DEBUG] Removing %s, filtered by targeting.", v)
g.Remove(v) g.Remove(v)
} }
} }

View File

@ -10,19 +10,34 @@ description: |-
# Resource Addressing # Resource Addressing
A __Resource Address__ is a string that references a specific resource in a A __Resource Address__ is a string that references a specific resource in a
larger infrastructure. The syntax of a resource address is: larger infrastructure. An address is made up of two parts:
``` ```
<resource_type>.<resource_name>[optional fields] [module path][resource spec]
``` ```
Required fields: __Module path__:
A module path addresses a module within the tree of modules. It takes the form:
```
module.A.module.B.module.C...
```
Multiple modules in a path indicate nesting. If a module path is specified
without a resource spec, the address applies to every resource within the
module. If the module path is omitted, this addresses the root module.
__Resource spec__:
A resource spec addresses a specific resource in the config. It takes the form:
```
resource_type.resource_name[N]
```
* `resource_type` - Type of the resource being addressed. * `resource_type` - Type of the resource being addressed.
* `resource_name` - User-defined name of the resource. * `resource_name` - User-defined name of the resource.
Optional fields may include:
* `[N]` - where `N` is a `0`-based index into a resource with multiple * `[N]` - where `N` is a `0`-based index into a resource with multiple
instances specified by the `count` meta-parameter. Omitting an index when instances specified by the `count` meta-parameter. Omitting an index when
addressing a resource where `count > 1` means that the address references addressing a resource where `count > 1` means that the address references