diff --git a/terraform/context_test.go b/terraform/context_test.go index 587ebb538..f72e602c4 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -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, ` + +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, ` + +module.child: + aws_instance.foo: + ID = foo + num = 2 + type = aws_instance + `) +} + func TestContext2Apply_unknownAttribute(t *testing.T) { m := testModule(t, "apply-unknown") p := testProvider("aws") diff --git a/terraform/graph_config_node_resource.go b/terraform/graph_config_node_resource.go index 7c94f098e..298e90167 100644 --- a/terraform/graph_config_node_resource.go +++ b/terraform/graph_config_node_resource.go @@ -19,6 +19,8 @@ type GraphNodeConfigResource struct { // Used during DynamicExpand to target indexes Targets []ResourceAddress + + Path []string } func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType { @@ -174,7 +176,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) // GraphNodeAddressable impl. func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress { return &ResourceAddress{ - // Indicates no specific index; will match on other three fields + Path: n.Path[1:], Index: -1, InstanceType: TypePrimary, Name: n.Resource.Name, diff --git a/terraform/resource_address.go b/terraform/resource_address.go index 01d7e0c02..583cdd2a0 100644 --- a/terraform/resource_address.go +++ b/terraform/resource_address.go @@ -2,14 +2,22 @@ package terraform import ( "fmt" + "reflect" "regexp" "strconv" + "strings" ) // 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 + // 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 + InstanceType InstanceType Name string Type string @@ -20,22 +28,18 @@ func ParseResourceAddress(s string) (*ResourceAddress, error) { 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 - } + resourceIndex, err := ParseResourceIndex(matches["index"]) + if 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 - } + instanceType, err := ParseInstanceType(matches["instance_type"]) + if err != nil { + return nil, err } + path := ParseResourcePath(matches["path"]) return &ResourceAddress{ + Path: path, Index: resourceIndex, InstanceType: instanceType, Name: matches["name"], @@ -49,19 +53,55 @@ func (addr *ResourceAddress) Equals(raw interface{}) bool { return false } + pathMatch := ((len(addr.Path) == 0 && len(other.Path) == 0) || + reflect.DeepEqual(addr.Path, other.Path)) + indexMatch := (addr.Index == -1 || other.Index == -1 || addr.Index == other.Index) - return (indexMatch && - addr.InstanceType == other.InstanceType && - addr.Name == other.Name && + nameMatch := (addr.Name == "" || + other.Name == "" || + addr.Name == other.Name) + + typeMatch := (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) { switch s { - case "primary": + case "", "primary": return TypePrimary, nil case "deposed": return TypeDeposed, nil @@ -76,10 +116,10 @@ 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[^.]+)\.` + - // "web" - `(?P[^.[]+)` + + // "module.foo.module.bar" (optional) + `(?P(?:module\.[^.]+\.?)*)` + + // "aws_instance.web" (optional when module path specified) + `(?:(?P[^.]+)\.(?P[^.[]+))?` + // "tainted" (optional, omission implies: "primary") `(?:\.(?P\w+))?` + // "1" (optional, omission implies: "0") diff --git a/terraform/resource_address_test.go b/terraform/resource_address_test.go index f6439b2e8..0b10a24fd 100644 --- a/terraform/resource_address_test.go +++ b/terraform/resource_address_test.go @@ -64,6 +64,46 @@ func TestParseResourceAddress(t *testing.T) { 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 { @@ -204,6 +244,57 @@ func TestResourceAddressEquals(t *testing.T) { }, 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 { diff --git a/terraform/test-fixtures/apply-targeted-module-resource/child/main.tf b/terraform/test-fixtures/apply-targeted-module-resource/child/main.tf new file mode 100644 index 000000000..7872c90fc --- /dev/null +++ b/terraform/test-fixtures/apply-targeted-module-resource/child/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + num = "2" +} + +resource "aws_instance" "bar" { + num = "2" +} diff --git a/terraform/test-fixtures/apply-targeted-module-resource/main.tf b/terraform/test-fixtures/apply-targeted-module-resource/main.tf new file mode 100644 index 000000000..88bf07f69 --- /dev/null +++ b/terraform/test-fixtures/apply-targeted-module-resource/main.tf @@ -0,0 +1,7 @@ +module "child" { + source = "./child" +} + +resource "aws_instance" "bar" { + foo = "bar" +} diff --git a/terraform/test-fixtures/apply-targeted-module/child/main.tf b/terraform/test-fixtures/apply-targeted-module/child/main.tf new file mode 100644 index 000000000..7872c90fc --- /dev/null +++ b/terraform/test-fixtures/apply-targeted-module/child/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + num = "2" +} + +resource "aws_instance" "bar" { + num = "2" +} diff --git a/terraform/test-fixtures/apply-targeted-module/main.tf b/terraform/test-fixtures/apply-targeted-module/main.tf new file mode 100644 index 000000000..938ce3a56 --- /dev/null +++ b/terraform/test-fixtures/apply-targeted-module/main.tf @@ -0,0 +1,11 @@ +module "child" { + source = "./child" +} + +resource "aws_instance" "foo" { + foo = "bar" +} + +resource "aws_instance" "bar" { + foo = "bar" +} diff --git a/terraform/transform_config.go b/terraform/transform_config.go index 3c061eeb1..a14e4f434 100644 --- a/terraform/transform_config.go +++ b/terraform/transform_config.go @@ -55,7 +55,10 @@ func (t *ConfigTransformer) Transform(g *Graph) error { // Write all the resources out 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 diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 76e65af73..0b56721b0 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -44,6 +44,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error { var node dag.Vertex = &graphNodeExpandedResource{ Index: index, Resource: t.Resource, + Path: g.Path, } if t.Destroy { node = &graphNodeExpandedResourceDestroy{ @@ -93,6 +94,7 @@ func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool { type graphNodeExpandedResource struct { Index int Resource *config.Resource + Path []string } func (n *graphNodeExpandedResource) Name() string { @@ -112,8 +114,8 @@ func (n *graphNodeExpandedResource) ResourceAddress() *ResourceAddress { index = 0 } return &ResourceAddress{ - Index: index, - // TODO: kjkjkj + Path: n.Path[1:], + Index: index, InstanceType: TypePrimary, Name: n.Resource.Name, Type: n.Resource.Type, diff --git a/terraform/transform_targets.go b/terraform/transform_targets.go index 1ddb7f9c4..84e3aed60 100644 --- a/terraform/transform_targets.go +++ b/terraform/transform_targets.go @@ -1,6 +1,10 @@ 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 // 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() { if _, ok := v.(GraphNodeAddressable); ok { if !targetedNodes.Include(v) { + log.Printf("[DEBUG] Removing %s, filtered by targeting.", v) g.Remove(v) } } diff --git a/website/source/docs/internals/resource-addressing.html.markdown b/website/source/docs/internals/resource-addressing.html.markdown index b4b994a88..d208475c0 100644 --- a/website/source/docs/internals/resource-addressing.html.markdown +++ b/website/source/docs/internals/resource-addressing.html.markdown @@ -10,19 +10,34 @@ description: |- # Resource Addressing 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: ``` -.[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_name` - User-defined name of the resource. - -Optional fields may include: - * `[N]` - where `N` is a `0`-based index into a resource with multiple instances specified by the `count` meta-parameter. Omitting an index when addressing a resource where `count > 1` means that the address references