diff --git a/command/apply.go b/command/apply.go index 62ed3dd9ab0a..105268a7e419 100644 --- a/command/apply.go +++ b/command/apply.go @@ -357,7 +357,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 5582ddf4f491..e1d39b7f01a3 100644 --- a/command/plan.go +++ b/command/plan.go @@ -198,7 +198,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 db577b361f14..3a19c6b1087a 100644 --- a/terraform/transform_targets.go +++ b/terraform/transform_targets.go @@ -17,6 +17,10 @@ type TargetsTransformer struct { // that already have the targets parsed ParsedTargets []ResourceAddress + // List of parsed excludes, provided by callers like ResourceCountTransform + // that already have the targets parsed + ParsedExcludes []ResourceAddress + // Set to true when we're in a `terraform destroy` or a // `terraform plan -destroy` Destroy bool @@ -24,23 +28,33 @@ type TargetsTransformer struct { func (t *TargetsTransformer) Transform(g *Graph) error { if len(t.Targets) > 0 && len(t.ParsedTargets) == 0 { - addrs, err := t.parseTargetAddresses() + targeted, excluded, err := t.parseTargetAddresses() if err != nil { return err } - t.ParsedTargets = addrs + t.ParsedTargets = targeted + t.ParsedExcludes = excluded } - if len(t.ParsedTargets) > 0 { + + if len(t.ParsedTargets) > 0 || len(t.ParsedExcludes) > 0 { targetedNodes, err := t.selectTargetedNodes(g, t.ParsedTargets) if err != nil { return err } + excludedNodes, err := t.selectTargetedNodes(g, t.ParsedExcludes) + 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 excluding.", dag.VertexName(v)) + g.Remove(v) } } } @@ -48,16 +62,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) + } +}