diff --git a/command/apply.go b/command/apply.go index 9c8c4904a2a7..672d72f6af55 100644 --- a/command/apply.go +++ b/command/apply.go @@ -351,7 +351,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 a38a228728c0..49225b1f505f 100644 --- a/command/plan.go +++ b/command/plan.go @@ -171,7 +171,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 125f9e302155..e751aacbfb38 100644 --- a/terraform/transform_targets.go +++ b/terraform/transform_targets.go @@ -41,6 +41,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 @@ -48,20 +52,25 @@ 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() { removable := false if _, ok := v.(GraphNodeResource); ok { @@ -70,9 +79,14 @@ func (t *TargetsTransformer) Transform(g *Graph) error { if vr, ok := v.(RemovableIfNotTargeted); ok { removable = vr.RemoveIfNotTargeted() } - if removable && !targetedNodes.Include(v) { - log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v)) - g.Remove(v) + if removable { + if len(t.ParsedTargets) > 0 && !targetedNodes.Include(v) { + log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v)) + g.Remove(v) + } else if len(t.ParsedExcludes) > 0 && excludedNodes.Include(v) { + log.Printf("[DEBUG] Removing %s, filtered by excluding.", dag.VertexName(v)) + g.Remove(v) + } } } } @@ -80,17 +94,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 c5c97cd22e6f..b71a554009eb 100644 --- a/terraform/transform_targets_test.go +++ b/terraform/transform_targets_test.go @@ -160,3 +160,101 @@ 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.me +aws_subnet.notme +aws_vpc.me +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_elb.me +aws_instance.metoo +aws_instance.notme +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_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) + } +}