diff --git a/command/apply.go b/command/apply.go index f924c65caa2a..d1ac0b8ac20d 100644 --- a/command/apply.go +++ b/command/apply.go @@ -332,7 +332,8 @@ Options: -target=resource Resource to target. Operation will be limited to this resource and its dependencies. This flag can be used - multiple times. + multiple times. Prefixing the resource with ! will + exclude the resource. -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/command/plan.go b/command/plan.go index 15c2b505f2f3..043ced00de5a 100644 --- a/command/plan.go +++ b/command/plan.go @@ -191,7 +191,8 @@ Options: -target=resource Resource to target. Operation will be limited to this resource and its dependencies. This flag can be used - multiple times. + multiple times. Prefixing the resource with ! will + exclude the resource. -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/terraform/transform_targets.go b/terraform/transform_targets.go index cab8c8b1ec55..454425b57616 100644 --- a/terraform/transform_targets.go +++ b/terraform/transform_targets.go @@ -21,21 +21,28 @@ type TargetsTransformer struct { func (t *TargetsTransformer) Transform(g *Graph) error { if len(t.Targets) > 0 { // TODO: duplicated in OrphanTransformer; pull up parsing earlier - addrs, err := t.parseTargetAddresses() + targeted, excluded, err := t.parseTargetAddresses() if err != nil { return err } - targetedNodes, err := t.selectTargetedNodes(g, addrs) + targetedNodes, err := t.selectTargetedNodes(g, targeted) + if err != nil { + return err + } + excludedNodes, err := t.selectTargetedNodes(g, excluded) if err != nil { return err } for _, v := range g.Vertices() { if _, ok := v.(GraphNodeAddressable); ok { - if !targetedNodes.Include(v) { + if targetedNodes.Len() > 0 && !targetedNodes.Include(v) { log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v)) g.Remove(v) + } else if excludedNodes.Len() > 0 && excludedNodes.Include(v) { + log.Printf("[DEBUG] Removing %s, filtered by targeting exclude.", dag.VertexName(v)) + g.Remove(v) } } } @@ -43,16 +50,25 @@ func (t *TargetsTransformer) Transform(g *Graph) error { return nil } -func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, error) { - addrs := make([]ResourceAddress, len(t.Targets)) - for i, target := range t.Targets { +func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, []ResourceAddress, error) { + var targeted, excluded []ResourceAddress + for _, target := range t.Targets { + exclude := string(target[0]) == "!" + if exclude { + target = target[1:] + log.Printf("[DEBUG] Excluding %s", target) + } ta, err := ParseResourceAddress(target) if err != nil { - return nil, err + return nil, nil, err + } + if exclude { + excluded = append(excluded, *ta) + } else { + targeted = append(targeted, *ta) } - addrs[i] = *ta } - return addrs, nil + return targeted, excluded, nil } // Returns the list of targeted nodes. A targeted node is either addressed diff --git a/terraform/transform_targets_test.go b/terraform/transform_targets_test.go index 2daa72827e5b..fc6d826810c4 100644 --- a/terraform/transform_targets_test.go +++ b/terraform/transform_targets_test.go @@ -69,3 +69,98 @@ aws_instance.metoo t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) } } + +func TestTargetsTransformer_exclude(t *testing.T) { + mod := testModule(t, "transform-targets-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &TargetsTransformer{Targets: []string{"!aws_instance.me"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +aws_instance.notme +aws_instance.notmeeither +aws_subnet.notme +aws_vpc.notme + `) + if actual != expected { + t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) + } +} + +func TestTargetsTransformer_exclude_destroy(t *testing.T) { + mod := testModule(t, "transform-targets-destroy") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &TargetsTransformer{ + Targets: []string{"!aws_instance.me"}, + Destroy: true, + } + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +aws_instance.notme +aws_subnet.notme + aws_vpc.notme +aws_vpc.notme + `) + if actual != expected { + t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) + } +} + +func TestTargetsTransformer_include_exclude(t *testing.T) { + mod := testModule(t, "transform-targets-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &TargetsTransformer{ + Targets: []string{ + "aws_instance.me", + "!aws_subnet.me", + }, + } + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +aws_instance.me + `) + if actual != expected { + t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) + } +}