From 97acccd3eda70f7552cecd71ea8f5916b215b694 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 24 Mar 2015 11:18:15 -0500 Subject: [PATCH] core: targeted operations Add `-target=resource` flag to core operations, allowing users to target specific resources in their infrastructure. When `-target` is used, the operation will only apply to that resource and its dependencies. The calculated dependencies are different depending on whether we're running a normal operation or a `terraform destroy`. Generally, "dependencies" refers to ancestors: resources falling _before_ the target in the graph, because their changes are required to accurately act on the target. For destroys, "dependencies" are descendents: those resources which fall _after_ the target. These resources depend on our target, which is going to be destroyed, so they should also be destroyed. --- command/apply.go | 16 +- command/apply_destroy_test.go | 90 ++++++ command/flag_kv.go | 14 + command/meta.go | 11 + command/plan.go | 7 +- command/refresh.go | 4 + .../apply-destroy-targeted/main.tf | 7 + dag/dag.go | 93 +++++- dag/dag_test.go | 62 ++++ helper/resource/testing.go | 3 +- terraform/context.go | 12 +- terraform/context_test.go | 266 ++++++++++-------- terraform/graph_builder.go | 11 + terraform/plan.go | 9 - .../transform-targets-basic/main.tf | 16 ++ .../transform-targets-destroy/main.tf | 18 ++ terraform/transform_targets.go | 77 +++++ terraform/transform_targets_test.go | 71 +++++ 18 files changed, 637 insertions(+), 150 deletions(-) create mode 100644 command/test-fixtures/apply-destroy-targeted/main.tf create mode 100644 terraform/test-fixtures/transform-targets-basic/main.tf create mode 100644 terraform/test-fixtures/transform-targets-destroy/main.tf create mode 100644 terraform/transform_targets.go create mode 100644 terraform/transform_targets_test.go diff --git a/command/apply.go b/command/apply.go index d46b71679..529d6e701 100644 --- a/command/apply.go +++ b/command/apply.go @@ -93,6 +93,7 @@ func (c *ApplyCommand) Run(args []string) int { // Build the context based on the arguments given ctx, planned, err := c.Context(contextOpts{ + Destroy: c.Destroy, Path: configPath, StatePath: c.Meta.statePath, }) @@ -140,12 +141,7 @@ func (c *ApplyCommand) Run(args []string) int { } } - var opts terraform.PlanOpts - if c.Destroy { - opts.Destroy = true - } - - if _, err := ctx.Plan(&opts); err != nil { + if _, err := ctx.Plan(); err != nil { c.Ui.Error(fmt.Sprintf( "Error creating plan: %s", err)) return 1 @@ -319,6 +315,10 @@ Options: "-state". This can be used to preserve the old state. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. @@ -357,6 +357,10 @@ Options: "-state". This can be used to preserve the old state. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/command/apply_destroy_test.go b/command/apply_destroy_test.go index bdc2440f0..63afb15ed 100644 --- a/command/apply_destroy_test.go +++ b/command/apply_destroy_test.go @@ -116,6 +116,96 @@ func TestApply_destroyPlan(t *testing.T) { } } +func TestApply_destroyTargeted(t *testing.T) { + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "i-ab123", + }, + }, + "test_load_balancer.foo": &terraform.ResourceState{ + Type: "test_load_balancer", + Primary: &terraform.InstanceState{ + ID: "lb-abc123", + }, + }, + }, + }, + }, + } + + statePath := testStateFile(t, originalState) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Destroy: true, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + // Run the apply command pointing to our existing state + args := []string{ + "-force", + "-target", "test_instance.foo", + "-state", statePath, + testFixturePath("apply-destroy-targeted"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Verify a new state exists + if _, err := os.Stat(statePath); err != nil { + t.Fatalf("err: %s", err) + } + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + state, err := terraform.ReadState(f) + if err != nil { + t.Fatalf("err: %s", err) + } + if state == nil { + t.Fatal("state should not be nil") + } + + actualStr := strings.TrimSpace(state.String()) + expectedStr := strings.TrimSpace(testApplyDestroyStr) + if actualStr != expectedStr { + t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) + } + + // Should have a backup file + f, err = os.Open(statePath + DefaultBackupExtention) + if err != nil { + t.Fatalf("err: %s", err) + } + + backupState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actualStr = strings.TrimSpace(backupState.String()) + expectedStr = strings.TrimSpace(originalState.String()) + if actualStr != expectedStr { + t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr) + } +} + const testApplyDestroyStr = ` ` diff --git a/command/flag_kv.go b/command/flag_kv.go index fd9b57b3a..6e0198778 100644 --- a/command/flag_kv.go +++ b/command/flag_kv.go @@ -85,3 +85,17 @@ func loadKVFile(rawPath string) (map[string]string, error) { return result, nil } + +// FlagStringSlice is a flag.Value implementation for parsing targets from the +// command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar + +type FlagStringSlice []string + +func (v *FlagStringSlice) String() string { + return "" +} +func (v *FlagStringSlice) Set(raw string) error { + *v = append(*v, raw) + + return nil +} diff --git a/command/meta.go b/command/meta.go index 28a01c542..b542304af 100644 --- a/command/meta.go +++ b/command/meta.go @@ -38,6 +38,9 @@ type Meta struct { input bool variables map[string]string + // Targets for this context (private) + targets []string + color bool oldUi cli.Ui @@ -126,6 +129,9 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { m.statePath = copts.StatePath } + // Tell the context if we're in a destroy plan / apply + opts.Destroy = copts.Destroy + // Store the loaded state state, err := m.State() if err != nil { @@ -267,6 +273,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { vs[k] = v } opts.Variables = vs + opts.Targets = m.targets opts.UIInput = m.UIInput() return &opts @@ -278,6 +285,7 @@ func (m *Meta) flagSet(n string) *flag.FlagSet { f.BoolVar(&m.input, "input", true, "input") f.Var((*FlagKV)(&m.variables), "var", "variables") f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file") + f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") if m.autoKey != "" { f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file") @@ -388,4 +396,7 @@ type contextOpts struct { // GetMode is the module.GetMode to use when loading the module tree. GetMode module.GetMode + + // Set to true when running a destroy plan/apply. + Destroy bool } diff --git a/command/plan.go b/command/plan.go index 24365d185..5c884d632 100644 --- a/command/plan.go +++ b/command/plan.go @@ -53,6 +53,7 @@ func (c *PlanCommand) Run(args []string) int { } ctx, _, err := c.Context(contextOpts{ + Destroy: destroy, Path: path, StatePath: c.Meta.statePath, }) @@ -86,7 +87,7 @@ func (c *PlanCommand) Run(args []string) int { } } - plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy}) + plan, err := ctx.Plan() if err != nil { c.Ui.Error(fmt.Sprintf("Error running plan: %s", err)) return 1 @@ -168,6 +169,10 @@ Options: up Terraform-managed resources. By default it will use the state "terraform.tfstate" if it exists. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/command/refresh.go b/command/refresh.go index 38d630050..32e795047 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -135,6 +135,10 @@ Options: -state-out=path Path to write updated state file. By default, the "-state" path will be used. + -target=resource Resource to target. Operation will be limited to this + resource and its dependencies. This flag can be used + multiple times. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/command/test-fixtures/apply-destroy-targeted/main.tf b/command/test-fixtures/apply-destroy-targeted/main.tf new file mode 100644 index 000000000..45ebc5b97 --- /dev/null +++ b/command/test-fixtures/apply-destroy-targeted/main.tf @@ -0,0 +1,7 @@ +resource "test_instance" "foo" { + count = 3 +} + +resource "test_load_balancer" "foo" { + instances = ["${test_instance.foo.*.id}"] +} diff --git a/dag/dag.go b/dag/dag.go index b81cb2874..0f53fb1f0 100644 --- a/dag/dag.go +++ b/dag/dag.go @@ -17,6 +17,40 @@ type AcyclicGraph struct { // WalkFunc is the callback used for walking the graph. type WalkFunc func(Vertex) error +// Returns a Set that includes every Vertex yielded by walking down from the +// provided starting Vertex v. +func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) { + s := new(Set) + start := asVertexList(g.DownEdges(v)) + memoFunc := func(v Vertex) error { + s.Add(v) + return nil + } + + if err := g.depthFirstWalk(start, memoFunc); err != nil { + return nil, err + } + + return s, nil +} + +// Returns a Set that includes every Vertex yielded by walking up from the +// provided starting Vertex v. +func (g *AcyclicGraph) Descendents(v Vertex) (*Set, error) { + s := new(Set) + start := asVertexList(g.UpEdges(v)) + memoFunc := func(v Vertex) error { + s.Add(v) + return nil + } + + if err := g.reverseDepthFirstWalk(start, memoFunc); err != nil { + return nil, err + } + + return s, nil +} + // Root returns the root of the DAG, or an error. // // Complexity: O(V) @@ -61,15 +95,11 @@ func (g *AcyclicGraph) TransitiveReduction() { for _, u := range g.Vertices() { uTargets := g.DownEdges(u) - vs := make([]Vertex, uTargets.Len()) - for i, vRaw := range uTargets.List() { - vs[i] = vRaw.(Vertex) - } + vs := asVertexList(g.DownEdges(u)) g.depthFirstWalk(vs, func(v Vertex) error { shared := uTargets.Intersection(g.DownEdges(v)) - for _, raw := range shared.List() { - vPrime := raw.(Vertex) + for _, vPrime := range asVertexList(shared) { g.RemoveEdge(BasicEdge(u, vPrime)) } @@ -145,12 +175,10 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error { for _, v := range vertices { // Build our list of dependencies and the list of channels to // wait on until we start executing for this vertex. - depsRaw := g.DownEdges(v).List() - deps := make([]Vertex, len(depsRaw)) + deps := asVertexList(g.DownEdges(v)) depChs := make([]<-chan struct{}, len(deps)) - for i, raw := range depsRaw { - deps[i] = raw.(Vertex) - depChs[i] = vertMap[deps[i]] + for i, dep := range deps { + depChs[i] = vertMap[dep] } // Get our channel so that we can close it when we're done @@ -200,6 +228,16 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error { return errs } +// simple convenience helper for converting a dag.Set to a []Vertex +func asVertexList(s *Set) []Vertex { + rawList := s.List() + vertexList := make([]Vertex, len(rawList)) + for i, raw := range rawList { + vertexList[i] = raw.(Vertex) + } + return vertexList +} + // depthFirstWalk does a depth-first walk of the graph starting from // the vertices in start. This is not exported now but it would make sense // to export this publicly at some point. @@ -233,3 +271,36 @@ func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error { return nil } + +// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from +// the vertices in start. +func (g *AcyclicGraph) reverseDepthFirstWalk(start []Vertex, cb WalkFunc) error { + seen := make(map[Vertex]struct{}) + frontier := make([]Vertex, len(start)) + copy(frontier, start) + for len(frontier) > 0 { + // Pop the current vertex + n := len(frontier) + current := frontier[n-1] + frontier = frontier[:n-1] + + // Check if we've seen this already and return... + if _, ok := seen[current]; ok { + continue + } + seen[current] = struct{}{} + + // Visit the current node + if err := cb(current); err != nil { + return err + } + + // Visit targets of this in reverse order. + targets := g.UpEdges(current).List() + for i := len(targets) - 1; i >= 0; i-- { + frontier = append(frontier, targets[i].(Vertex)) + } + } + + return nil +} diff --git a/dag/dag_test.go b/dag/dag_test.go index feead7968..e7b2db8d2 100644 --- a/dag/dag_test.go +++ b/dag/dag_test.go @@ -126,6 +126,68 @@ func TestAcyclicGraphValidate_cycleSelf(t *testing.T) { } } +func TestAcyclicGraphAncestors(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Add(4) + g.Add(5) + g.Connect(BasicEdge(0, 1)) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 3)) + g.Connect(BasicEdge(3, 4)) + g.Connect(BasicEdge(4, 5)) + + actual, err := g.Ancestors(2) + if err != nil { + t.Fatalf("err: %#v", err) + } + + expected := []Vertex{3, 4, 5} + + if actual.Len() != len(expected) { + t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected)) + } + + for _, e := range expected { + if !actual.Include(e) { + t.Fatalf("expected: %#v to include: %#v", expected, actual) + } + } +} + +func TestAcyclicGraphDescendents(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Add(4) + g.Add(5) + g.Connect(BasicEdge(0, 1)) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 3)) + g.Connect(BasicEdge(3, 4)) + g.Connect(BasicEdge(4, 5)) + + actual, err := g.Descendents(2) + if err != nil { + t.Fatalf("err: %#v", err) + } + + expected := []Vertex{0, 1} + + if actual.Len() != len(expected) { + t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected)) + } + + for _, e := range expected { + if !actual.Include(e) { + t.Fatalf("expected: %#v to include: %#v", expected, actual) + } + } +} + func TestAcyclicGraphWalk(t *testing.T) { var g AcyclicGraph g.Add(1) diff --git a/helper/resource/testing.go b/helper/resource/testing.go index cedadfc72..90cfc175f 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -190,6 +190,7 @@ func testStep( // Build the context opts.Module = mod opts.State = state + opts.Destroy = step.Destroy ctx := terraform.NewContext(&opts) if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { estrs := make([]string, len(es)) @@ -209,7 +210,7 @@ func testStep( } // Plan! - if p, err := ctx.Plan(&terraform.PlanOpts{Destroy: step.Destroy}); err != nil { + if p, err := ctx.Plan(); err != nil { return state, fmt.Errorf( "Error planning: %s", err) } else { diff --git a/terraform/context.go b/terraform/context.go index 86a804548..6beaab636 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -33,6 +33,7 @@ const ( // ContextOpts are the user-configurable options to create a context with // NewContext. type ContextOpts struct { + Destroy bool Diff *Diff Hooks []Hook Module *module.Tree @@ -40,6 +41,7 @@ type ContextOpts struct { State *State Providers map[string]ResourceProviderFactory Provisioners map[string]ResourceProvisionerFactory + Targets []string Variables map[string]string UIInput UIInput @@ -49,6 +51,7 @@ type ContextOpts struct { // perform operations on infrastructure. This structure is built using // NewContext. See the documentation for that. type Context struct { + destroy bool diff *Diff diffLock sync.RWMutex hooks []Hook @@ -58,6 +61,7 @@ type Context struct { sh *stopHook state *State stateLock sync.RWMutex + targets []string uiInput UIInput variables map[string]string @@ -95,12 +99,14 @@ func NewContext(opts *ContextOpts) *Context { } return &Context{ + destroy: opts.Destroy, diff: opts.Diff, hooks: hooks, module: opts.Module, providers: opts.Providers, provisioners: opts.Provisioners, state: state, + targets: opts.Targets, uiInput: opts.UIInput, variables: opts.Variables, @@ -135,6 +141,8 @@ func (c *Context) GraphBuilder() GraphBuilder { Providers: providers, Provisioners: provisioners, State: c.state, + Targets: c.targets, + Destroy: c.destroy, } } @@ -253,7 +261,7 @@ func (c *Context) Apply() (*State, error) { // // Plan also updates the diff of this context to be the diff generated // by the plan, so Apply can be called after. -func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { +func (c *Context) Plan() (*Plan, error) { v := c.acquireRun() defer c.releaseRun(v) @@ -264,7 +272,7 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { } var operation walkOperation - if opts != nil && opts.Destroy { + if c.destroy { operation = walkPlanDestroy } else { // Set our state to be something temporary. We do this so that diff --git a/terraform/context_test.go b/terraform/context_test.go index abffbf5c3..238f04d04 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -24,7 +24,7 @@ func TestContext2Plan(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -57,7 +57,7 @@ func TestContext2Plan_emptyDiff(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -80,7 +80,7 @@ func TestContext2Plan_minimal(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -103,7 +103,7 @@ func TestContext2Plan_modules(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -126,7 +126,7 @@ func TestContext2Plan_moduleInput(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -149,7 +149,7 @@ func TestContext2Plan_moduleInputComputed(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -175,7 +175,7 @@ func TestContext2Plan_moduleInputFromVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -198,7 +198,7 @@ func TestContext2Plan_moduleMultiVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -237,7 +237,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -282,7 +282,7 @@ func TestContext2Plan_moduleProviderInherit(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -332,7 +332,7 @@ func TestContext2Plan_moduleProviderDefaults(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -385,7 +385,7 @@ func TestContext2Plan_moduleProviderDefaultsVar(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -410,7 +410,7 @@ func TestContext2Plan_moduleVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -433,7 +433,7 @@ func TestContext2Plan_moduleVarComputed(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -471,7 +471,7 @@ func TestContext2Plan_nil(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -491,7 +491,7 @@ func TestContext2Plan_computed(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -514,7 +514,7 @@ func TestContext2Plan_computedList(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -537,7 +537,7 @@ func TestContext2Plan_count(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -564,7 +564,7 @@ func TestContext2Plan_countComputed(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err == nil { t.Fatal("should error") } @@ -581,7 +581,7 @@ func TestContext2Plan_countIndex(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -604,7 +604,7 @@ func TestContext2Plan_countIndexZero(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -630,7 +630,7 @@ func TestContext2Plan_countVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -653,7 +653,7 @@ func TestContext2Plan_countZero(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -676,7 +676,7 @@ func TestContext2Plan_countOneIndex(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -731,7 +731,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -774,7 +774,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -817,7 +817,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -875,7 +875,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -917,10 +917,11 @@ func TestContext2Plan_destroy(t *testing.T) { Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), }, - State: s, + State: s, + Destroy: true, }) - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -971,10 +972,11 @@ func TestContext2Plan_moduleDestroy(t *testing.T) { Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), }, - State: s, + State: s, + Destroy: true, }) - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1020,10 +1022,11 @@ func TestContext2Plan_moduleDestroyMultivar(t *testing.T) { Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), }, - State: s, + State: s, + Destroy: true, }) - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1051,7 +1054,7 @@ func TestContext2Plan_pathVar(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1118,7 +1121,7 @@ func TestContext2Plan_diffVar(t *testing.T) { }, nil } - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1143,7 +1146,7 @@ func TestContext2Plan_hook(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1183,7 +1186,7 @@ func TestContext2Plan_orphan(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1221,7 +1224,7 @@ func TestContext2Plan_state(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1273,7 +1276,7 @@ func TestContext2Plan_taint(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1324,7 +1327,7 @@ func TestContext2Plan_multiple_taint(t *testing.T) { State: s, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1357,7 +1360,7 @@ func TestContext2Plan_provider(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -1377,7 +1380,7 @@ func TestContext2Plan_varMultiCountOne(t *testing.T) { }, }) - plan, err := ctx.Plan(nil) + plan, err := ctx.Plan() if err != nil { t.Fatalf("err: %s", err) } @@ -1399,7 +1402,7 @@ func TestContext2Plan_varListErr(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err == nil { t.Fatal("should error") } @@ -2468,7 +2471,7 @@ func TestContext2Input(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2513,7 +2516,7 @@ func TestContext2Input_provider(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2590,7 +2593,7 @@ func TestContext2Input_providerId(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2638,7 +2641,7 @@ func TestContext2Input_providerOnly(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2693,7 +2696,7 @@ func TestContext2Input_providerVars(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2741,7 +2744,7 @@ func TestContext2Input_varOnly(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2787,7 +2790,7 @@ func TestContext2Input_varOnlyUnset(t *testing.T) { t.Fatalf("err: %s", err) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2815,7 +2818,7 @@ func TestContext2Apply(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2848,7 +2851,7 @@ func TestContext2Apply_emptyModule(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -2896,7 +2899,7 @@ func TestContext2Apply_createBeforeDestroy(t *testing.T) { State: state, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf(p.String()) @@ -2950,7 +2953,7 @@ func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { State: state, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf(p.String()) @@ -2985,7 +2988,7 @@ func TestContext2Apply_minimal(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3013,7 +3016,7 @@ func TestContext2Apply_badDiff(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3071,7 +3074,7 @@ func TestContext2Apply_cancel(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3112,7 +3115,7 @@ func TestContext2Apply_compute(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3181,7 +3184,7 @@ func TestContext2Apply_countDecrease(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3241,7 +3244,7 @@ func TestContext2Apply_countDecreaseToOne(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3303,7 +3306,7 @@ func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { State: s, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { testStringMatch(t, p, testTerraformApplyCountDecToOneCorruptedPlanStr) @@ -3354,7 +3357,7 @@ func TestContext2Apply_countTainted(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3382,7 +3385,7 @@ func TestContext2Apply_countVariable(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3410,7 +3413,7 @@ func TestContext2Apply_module(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3439,9 +3442,10 @@ func TestContext2Apply_moduleVarResourceCount(t *testing.T) { Variables: map[string]string{ "count": "2", }, + Destroy: true, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3459,7 +3463,7 @@ func TestContext2Apply_moduleVarResourceCount(t *testing.T) { }, }) - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3481,7 +3485,7 @@ func TestContext2Apply_moduleBool(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3515,7 +3519,7 @@ func TestContext2Apply_multiProvider(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3548,7 +3552,7 @@ func TestContext2Apply_nilDiff(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3588,7 +3592,7 @@ func TestContext2Apply_Provisioner_compute(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3633,7 +3637,7 @@ func TestContext2Apply_provisionerCreateFail(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3672,7 +3676,7 @@ func TestContext2Apply_provisionerCreateFailNoId(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3712,7 +3716,7 @@ func TestContext2Apply_provisionerFail(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3767,7 +3771,7 @@ func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3816,7 +3820,7 @@ func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { } p.DiffFn = testDiffFn - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3874,7 +3878,7 @@ func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) { } p.DiffFn = testDiffFn - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3931,7 +3935,7 @@ func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) { } } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3955,7 +3959,7 @@ aws_instance.web: (1 deposed) State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -3983,7 +3987,7 @@ aws_instance.web: (2 deposed) } createdInstanceId = "qux" - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } state, err = ctx.Apply() @@ -4005,7 +4009,7 @@ aws_instance.web: (1 deposed) } createdInstanceId = "quux" - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } state, err = ctx.Apply() @@ -4045,7 +4049,7 @@ func TestContext2Apply_provisionerResourceRef(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4091,7 +4095,7 @@ func TestContext2Apply_provisionerSelfRef(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4144,7 +4148,7 @@ func TestContext2Apply_provisionerMultiSelfRef(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4192,7 +4196,7 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4229,7 +4233,7 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4300,7 +4304,7 @@ func TestContext2Apply_outputDiffVars(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } if _, err := ctx.Apply(); err != nil { @@ -4363,7 +4367,7 @@ func TestContext2Apply_Provisioner_ConnInfo(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4399,22 +4403,32 @@ func TestContext2Apply_destroy(t *testing.T) { }) // First plan and apply a create operation - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - if _, err := ctx.Apply(); err != nil { + state, err := ctx.Apply() + if err != nil { t.Fatalf("err: %s", err) } // Next, plan and apply a destroy operation - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + h.Active = true + ctx = testContext2(t, &ContextOpts{ + Destroy: true, + State: state, + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - h.Active = true - - state, err := ctx.Apply() + state, err = ctx.Apply() if err != nil { t.Fatalf("err: %s", err) } @@ -4430,7 +4444,7 @@ func TestContext2Apply_destroy(t *testing.T) { expected2 := []string{"aws_instance.bar", "aws_instance.foo"} actual2 := h.IDs if !reflect.DeepEqual(actual2, expected2) { - t.Fatalf("bad: %#v", actual2) + t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2) } } @@ -4449,22 +4463,33 @@ func TestContext2Apply_destroyOutputs(t *testing.T) { }) // First plan and apply a create operation - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - if _, err := ctx.Apply(); err != nil { + state, err := ctx.Apply() + + if err != nil { t.Fatalf("err: %s", err) } // Next, plan and apply a destroy operation - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + h.Active = true + ctx = testContext2(t, &ContextOpts{ + Destroy: true, + State: state, + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } - h.Active = true - - state, err := ctx.Apply() + state, err = ctx.Apply() if err != nil { t.Fatalf("err: %s", err) } @@ -4520,7 +4545,7 @@ func TestContext2Apply_destroyOrphan(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4577,10 +4602,11 @@ func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) { Provisioners: map[string]ResourceProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, - State: s, + State: s, + Destroy: true, }) - if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4638,7 +4664,7 @@ func TestContext2Apply_error(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4705,7 +4731,7 @@ func TestContext2Apply_errorPartial(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4740,7 +4766,7 @@ func TestContext2Apply_hook(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4788,7 +4814,7 @@ func TestContext2Apply_idAttr(t *testing.T) { }, nil } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4822,7 +4848,7 @@ func TestContext2Apply_output(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4850,7 +4876,7 @@ func TestContext2Apply_outputInvalid(t *testing.T) { }, }) - _, err := ctx.Plan(nil) + _, err := ctx.Plan() if err == nil { t.Fatalf("err: %s", err) } @@ -4871,7 +4897,7 @@ func TestContext2Apply_outputList(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4899,7 +4925,7 @@ func TestContext2Apply_outputMulti(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4927,7 +4953,7 @@ func TestContext2Apply_outputMultiIndex(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -4992,7 +5018,7 @@ func TestContext2Apply_taint(t *testing.T) { State: s, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5057,7 +5083,7 @@ func TestContext2Apply_taintDep(t *testing.T) { State: s, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf("plan: %s", p) @@ -5120,7 +5146,7 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) { State: s, }) - if p, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } else { t.Logf("plan: %s", p) @@ -5150,7 +5176,7 @@ func TestContext2Apply_unknownAttribute(t *testing.T) { }, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5190,7 +5216,7 @@ func TestContext2Apply_vars(t *testing.T) { t.Fatalf("bad: %s", e) } - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5248,7 +5274,7 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } @@ -5357,7 +5383,7 @@ func TestContext2Apply_singleDestroy(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if _, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index 4d5726954..d42f6bfe0 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -65,6 +65,13 @@ type BuiltinGraphBuilder struct { // Provisioners is the list of provisioners supported. Provisioners []string + + // Targets is the user-specified list of resources to target. + Targets []string + + // Destroy is set to true when we're in a `terraform destroy` or a + // `terraform plan -destroy` + Destroy bool } // Build builds the graph according to the steps returned by Steps. @@ -104,6 +111,10 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { }, }, + // Optionally reduces the graph to a user-specified list of targets and + // their dependencies. + &TargetsTransformer{Targets: b.Targets, Destroy: b.Destroy}, + // Create the destruction nodes &DestroyTransformer{}, &CreateBeforeDestroyTransformer{}, diff --git a/terraform/plan.go b/terraform/plan.go index e73fde383..715136edc 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -18,15 +18,6 @@ func init() { gob.Register(make(map[string]string)) } -// PlanOpts are the options used to generate an execution plan for -// Terraform. -type PlanOpts struct { - // If set to true, then the generated plan will destroy all resources - // that are created. Otherwise, it will move towards the desired state - // specified in the configuration. - Destroy bool -} - // Plan represents a single Terraform execution plan, which contains // all the information necessary to make an infrastructure change. type Plan struct { diff --git a/terraform/test-fixtures/transform-targets-basic/main.tf b/terraform/test-fixtures/transform-targets-basic/main.tf new file mode 100644 index 000000000..b845a1de6 --- /dev/null +++ b/terraform/test-fixtures/transform-targets-basic/main.tf @@ -0,0 +1,16 @@ +resource "aws_vpc" "me" {} + +resource "aws_subnet" "me" { + vpc_id = "${aws_vpc.me.id}" +} + +resource "aws_instance" "me" { + subnet_id = "${aws_subnet.me.id}" +} + +resource "aws_vpc" "notme" {} +resource "aws_subnet" "notme" {} +resource "aws_instance" "notme" {} +resource "aws_instance" "notmeeither" { + name = "${aws_instance.me.id}" +} diff --git a/terraform/test-fixtures/transform-targets-destroy/main.tf b/terraform/test-fixtures/transform-targets-destroy/main.tf new file mode 100644 index 000000000..da99de43c --- /dev/null +++ b/terraform/test-fixtures/transform-targets-destroy/main.tf @@ -0,0 +1,18 @@ +resource "aws_vpc" "notme" {} + +resource "aws_subnet" "notme" { + vpc_id = "${aws_vpc.notme.id}" +} + +resource "aws_instance" "me" { + subnet_id = "${aws_subnet.notme.id}" +} + +resource "aws_instance" "notme" {} +resource "aws_instance" "metoo" { + name = "${aws_instance.me.id}" +} + +resource "aws_elb" "me" { + instances = "${aws_instance.me.*.id}" +} diff --git a/terraform/transform_targets.go b/terraform/transform_targets.go new file mode 100644 index 000000000..0897ef7f9 --- /dev/null +++ b/terraform/transform_targets.go @@ -0,0 +1,77 @@ +package terraform + +import "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 +// their dependencies. +type TargetsTransformer struct { + // List of targeted resource names specified by the user + Targets []string + + // Set to true when we're in a `terraform destroy` or a + // `terraform plan -destroy` + Destroy bool +} + +func (t *TargetsTransformer) Transform(g *Graph) error { + if len(t.Targets) > 0 { + targetedNodes, err := t.selectTargetedNodes(g) + if err != nil { + return err + } + + for _, v := range g.Vertices() { + if !targetedNodes.Include(v) { + g.Remove(v) + } + } + } + return nil +} + +func (t *TargetsTransformer) selectTargetedNodes(g *Graph) (*dag.Set, error) { + targetedNodes := new(dag.Set) + for _, v := range g.Vertices() { + // Keep all providers; they'll be pruned later if necessary + if r, ok := v.(GraphNodeProvider); ok { + targetedNodes.Add(r) + continue + } + + // For the remaining filter, we only care about Resources and their deps + r, ok := v.(*GraphNodeConfigResource) + if !ok { + continue + } + + if t.resourceIsTarget(r) { + targetedNodes.Add(r) + + var deps *dag.Set + var err error + if t.Destroy { + deps, err = g.Descendents(r) + } else { + deps, err = g.Ancestors(r) + } + if err != nil { + return nil, err + } + + for _, d := range deps.List() { + targetedNodes.Add(d) + } + } + } + return targetedNodes, nil +} + +func (t *TargetsTransformer) resourceIsTarget(r *GraphNodeConfigResource) bool { + for _, target := range t.Targets { + if target == r.Name() { + return true + } + } + return false +} diff --git a/terraform/transform_targets_test.go b/terraform/transform_targets_test.go new file mode 100644 index 000000000..2daa72827 --- /dev/null +++ b/terraform/transform_targets_test.go @@ -0,0 +1,71 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestTargetsTransformer(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.me + aws_subnet.me +aws_subnet.me + aws_vpc.me +aws_vpc.me + `) + if actual != expected { + t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) + } +} + +func TestTargetsTransformer_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.me +aws_instance.me +aws_instance.metoo + aws_instance.me + `) + if actual != expected { + t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual) + } +}