mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #1293 from hashicorp/f-targeted-ops
core: targeted operations
This commit is contained in:
commit
5b699fea9e
@ -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.
|
||||
|
||||
|
@ -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 = `
|
||||
<no state>
|
||||
`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
7
command/test-fixtures/apply-destroy-targeted/main.tf
Normal file
7
command/test-fixtures/apply-destroy-targeted/main.tf
Normal file
@ -0,0 +1,7 @@
|
||||
resource "test_instance" "foo" {
|
||||
count = 3
|
||||
}
|
||||
|
||||
resource "test_load_balancer" "foo" {
|
||||
instances = ["${test_instance.foo.*.id}"]
|
||||
}
|
93
dag/dag.go
93
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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
@ -82,7 +89,11 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
|
||||
return []GraphTransformer{
|
||||
// Create all our resources from the configuration and state
|
||||
&ConfigTransformer{Module: b.Root},
|
||||
&OrphanTransformer{State: b.State, Module: b.Root},
|
||||
&OrphanTransformer{
|
||||
State: b.State,
|
||||
Module: b.Root,
|
||||
Targeting: (len(b.Targets) > 0),
|
||||
},
|
||||
|
||||
// Provider-related transformations
|
||||
&MissingProviderTransformer{Providers: b.Providers},
|
||||
@ -104,6 +115,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{},
|
||||
|
@ -21,6 +21,26 @@ type graphNodeConfig interface {
|
||||
GraphNodeDependent
|
||||
}
|
||||
|
||||
// GraphNodeAddressable is an interface that all graph nodes for the
|
||||
// configuration graph need to implement in order to be be addressed / targeted
|
||||
// properly.
|
||||
type GraphNodeAddressable interface {
|
||||
graphNodeConfig
|
||||
|
||||
ResourceAddress() *ResourceAddress
|
||||
}
|
||||
|
||||
// GraphNodeTargetable is an interface for graph nodes to implement when they
|
||||
// need to be told about incoming targets. This is useful for nodes that need
|
||||
// to respect targets as they dynamically expand. Note that the list of targets
|
||||
// provided will contain every target provided, and each implementing graph
|
||||
// node must filter this list to targets considered relevant.
|
||||
type GraphNodeTargetable interface {
|
||||
GraphNodeAddressable
|
||||
|
||||
SetTargets([]ResourceAddress)
|
||||
}
|
||||
|
||||
// GraphNodeConfigModule represents a module within the configuration graph.
|
||||
type GraphNodeConfigModule struct {
|
||||
Path []string
|
||||
@ -191,6 +211,9 @@ type GraphNodeConfigResource struct {
|
||||
// If this is set to anything other than destroyModeNone, then this
|
||||
// resource represents a resource that will be destroyed in some way.
|
||||
DestroyMode GraphNodeDestroyMode
|
||||
|
||||
// Used during DynamicExpand to target indexes
|
||||
Targets []ResourceAddress
|
||||
}
|
||||
|
||||
func (n *GraphNodeConfigResource) DependableName() []string {
|
||||
@ -279,6 +302,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||
steps = append(steps, &ResourceCountTransformer{
|
||||
Resource: n.Resource,
|
||||
Destroy: n.DestroyMode != DestroyNone,
|
||||
Targets: n.Targets,
|
||||
})
|
||||
}
|
||||
|
||||
@ -289,8 +313,9 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||
// expand orphans, which have all the same semantics in a destroy
|
||||
// as a primary.
|
||||
steps = append(steps, &OrphanTransformer{
|
||||
State: state,
|
||||
View: n.Resource.Id(),
|
||||
State: state,
|
||||
View: n.Resource.Id(),
|
||||
Targeting: (len(n.Targets) > 0),
|
||||
})
|
||||
|
||||
steps = append(steps, &DeposedTransformer{
|
||||
@ -314,6 +339,22 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
||||
return b.Build(ctx.Path())
|
||||
}
|
||||
|
||||
// GraphNodeAddressable impl.
|
||||
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
|
||||
return &ResourceAddress{
|
||||
// Indicates no specific index; will match on other three fields
|
||||
Index: -1,
|
||||
InstanceType: TypePrimary,
|
||||
Name: n.Resource.Name,
|
||||
Type: n.Resource.Type,
|
||||
}
|
||||
}
|
||||
|
||||
// GraphNodeTargetable impl.
|
||||
func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) {
|
||||
n.Targets = targets
|
||||
}
|
||||
|
||||
// GraphNodeEvalable impl.
|
||||
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
||||
return &EvalSequence{
|
||||
|
13
terraform/instancetype.go
Normal file
13
terraform/instancetype.go
Normal file
@ -0,0 +1,13 @@
|
||||
package terraform
|
||||
|
||||
//go:generate stringer -type=InstanceType instancetype.go
|
||||
|
||||
// InstanceType is an enum of the various types of instances store in the State
|
||||
type InstanceType int
|
||||
|
||||
const (
|
||||
TypeInvalid InstanceType = iota
|
||||
TypePrimary
|
||||
TypeTainted
|
||||
TypeDeposed
|
||||
)
|
16
terraform/instancetype_string.go
Normal file
16
terraform/instancetype_string.go
Normal file
@ -0,0 +1,16 @@
|
||||
// generated by stringer -type=InstanceType instancetype.go; DO NOT EDIT
|
||||
|
||||
package terraform
|
||||
|
||||
import "fmt"
|
||||
|
||||
const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed"
|
||||
|
||||
var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44}
|
||||
|
||||
func (i InstanceType) String() string {
|
||||
if i < 0 || i+1 >= InstanceType(len(_InstanceType_index)) {
|
||||
return fmt.Sprintf("InstanceType(%d)", i)
|
||||
}
|
||||
return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]]
|
||||
}
|
@ -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 {
|
||||
|
98
terraform/resource_address.go
Normal file
98
terraform/resource_address.go
Normal file
@ -0,0 +1,98 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// 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
|
||||
InstanceType InstanceType
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
func ParseResourceAddress(s string) (*ResourceAddress, error) {
|
||||
matches, err := tokenizeResourceAddress(s)
|
||||
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
|
||||
}
|
||||
}
|
||||
instanceType := TypePrimary
|
||||
if matches["instance_type"] != "" {
|
||||
var err error
|
||||
if instanceType, err = ParseInstanceType(matches["instance_type"]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &ResourceAddress{
|
||||
Index: resourceIndex,
|
||||
InstanceType: instanceType,
|
||||
Name: matches["name"],
|
||||
Type: matches["type"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (addr *ResourceAddress) Equals(raw interface{}) bool {
|
||||
other, ok := raw.(*ResourceAddress)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
indexMatch := (addr.Index == -1 ||
|
||||
other.Index == -1 ||
|
||||
addr.Index == other.Index)
|
||||
|
||||
return (indexMatch &&
|
||||
addr.InstanceType == other.InstanceType &&
|
||||
addr.Name == other.Name &&
|
||||
addr.Type == other.Type)
|
||||
}
|
||||
|
||||
func ParseInstanceType(s string) (InstanceType, error) {
|
||||
switch s {
|
||||
case "primary":
|
||||
return TypePrimary, nil
|
||||
case "deposed":
|
||||
return TypeDeposed, nil
|
||||
case "tainted":
|
||||
return TypeTainted, nil
|
||||
default:
|
||||
return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
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<type>\w+)\.` +
|
||||
// "web"
|
||||
`(?P<name>\w+)` +
|
||||
// "tainted" (optional, omission implies: "primary")
|
||||
`(?:\.(?P<instance_type>\w+))?` +
|
||||
// "1" (optional, omission implies: "0")
|
||||
`(?:\[(?P<index>\d+)\])?` +
|
||||
`\z`)
|
||||
groupNames := re.SubexpNames()
|
||||
rawMatches := re.FindAllStringSubmatch(s, -1)
|
||||
if len(rawMatches) != 1 {
|
||||
return nil, fmt.Errorf("Problem parsing address: %q", s)
|
||||
}
|
||||
matches := make(map[string]string)
|
||||
for i, m := range rawMatches[0] {
|
||||
matches[groupNames[i]] = m
|
||||
}
|
||||
return matches, nil
|
||||
}
|
207
terraform/resource_address_test.go
Normal file
207
terraform/resource_address_test.go
Normal file
@ -0,0 +1,207 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseResourceAddress(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Input string
|
||||
Expected *ResourceAddress
|
||||
}{
|
||||
"implicit primary, no specific index": {
|
||||
Input: "aws_instance.foo",
|
||||
Expected: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: -1,
|
||||
},
|
||||
},
|
||||
"implicit primary, explicit index": {
|
||||
Input: "aws_instance.foo[2]",
|
||||
Expected: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 2,
|
||||
},
|
||||
},
|
||||
"explicit primary, explicit index": {
|
||||
Input: "aws_instance.foo.primary[2]",
|
||||
Expected: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 2,
|
||||
},
|
||||
},
|
||||
"tainted": {
|
||||
Input: "aws_instance.foo.tainted",
|
||||
Expected: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypeTainted,
|
||||
Index: -1,
|
||||
},
|
||||
},
|
||||
"deposed": {
|
||||
Input: "aws_instance.foo.deposed",
|
||||
Expected: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypeDeposed,
|
||||
Index: -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
out, err := ParseResourceAddress(tc.Input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %#v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(out, tc.Expected) {
|
||||
t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceAddressEquals(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Address *ResourceAddress
|
||||
Other interface{}
|
||||
Expect bool
|
||||
}{
|
||||
"basic match": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Expect: true,
|
||||
},
|
||||
"address does not set index": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: -1,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 3,
|
||||
},
|
||||
Expect: true,
|
||||
},
|
||||
"other does not set index": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 3,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: -1,
|
||||
},
|
||||
Expect: true,
|
||||
},
|
||||
"neither sets index": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: -1,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: -1,
|
||||
},
|
||||
Expect: true,
|
||||
},
|
||||
"different type": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_vpc",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Expect: false,
|
||||
},
|
||||
"different name": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "bar",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Expect: false,
|
||||
},
|
||||
"different instance type": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypeTainted,
|
||||
Index: 0,
|
||||
},
|
||||
Expect: false,
|
||||
},
|
||||
"different index": {
|
||||
Address: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 0,
|
||||
},
|
||||
Other: &ResourceAddress{
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
InstanceType: TypePrimary,
|
||||
Index: 1,
|
||||
},
|
||||
Expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
actual := tc.Address.Equals(tc.Other)
|
||||
if actual != tc.Expect {
|
||||
t.Fatalf("%q: expected equals: %t, got %t for:\n%#v\n%#v",
|
||||
tn, tc.Expect, actual, tc.Address, tc.Other)
|
||||
}
|
||||
}
|
||||
}
|
7
terraform/test-fixtures/apply-targeted-count/main.tf
Normal file
7
terraform/test-fixtures/apply-targeted-count/main.tf
Normal file
@ -0,0 +1,7 @@
|
||||
resource "aws_instance" "foo" {
|
||||
count = 3
|
||||
}
|
||||
|
||||
resource "aws_instance" "bar" {
|
||||
count = 3
|
||||
}
|
7
terraform/test-fixtures/apply-targeted/main.tf
Normal file
7
terraform/test-fixtures/apply-targeted/main.tf
Normal file
@ -0,0 +1,7 @@
|
||||
resource "aws_instance" "foo" {
|
||||
num = "2"
|
||||
}
|
||||
|
||||
resource "aws_instance" "bar" {
|
||||
foo = "bar"
|
||||
}
|
7
terraform/test-fixtures/plan-targeted/main.tf
Normal file
7
terraform/test-fixtures/plan-targeted/main.tf
Normal file
@ -0,0 +1,7 @@
|
||||
resource "aws_instance" "foo" {
|
||||
num = "2"
|
||||
}
|
||||
|
||||
resource "aws_instance" "bar" {
|
||||
foo = "${aws_instance.foo.num}"
|
||||
}
|
9
terraform/test-fixtures/refresh-targeted-count/main.tf
Normal file
9
terraform/test-fixtures/refresh-targeted-count/main.tf
Normal file
@ -0,0 +1,9 @@
|
||||
resource "aws_vpc" "metoo" {}
|
||||
resource "aws_instance" "notme" { }
|
||||
resource "aws_instance" "me" {
|
||||
vpc_id = "${aws_vpc.metoo.id}"
|
||||
count = 3
|
||||
}
|
||||
resource "aws_elb" "meneither" {
|
||||
instances = ["${aws_instance.me.*.id}"]
|
||||
}
|
8
terraform/test-fixtures/refresh-targeted/main.tf
Normal file
8
terraform/test-fixtures/refresh-targeted/main.tf
Normal file
@ -0,0 +1,8 @@
|
||||
resource "aws_vpc" "metoo" {}
|
||||
resource "aws_instance" "notme" { }
|
||||
resource "aws_instance" "me" {
|
||||
vpc_id = "${aws_vpc.metoo.id}"
|
||||
}
|
||||
resource "aws_elb" "meneither" {
|
||||
instances = ["${aws_instance.me.*.id}"]
|
||||
}
|
16
terraform/test-fixtures/transform-targets-basic/main.tf
Normal file
16
terraform/test-fixtures/transform-targets-basic/main.tf
Normal file
@ -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}"
|
||||
}
|
18
terraform/test-fixtures/transform-targets-destroy/main.tf
Normal file
18
terraform/test-fixtures/transform-targets-destroy/main.tf
Normal file
@ -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}"
|
||||
}
|
@ -2,6 +2,7 @@ package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
@ -25,6 +26,11 @@ type OrphanTransformer struct {
|
||||
// using the graph path.
|
||||
Module *module.Tree
|
||||
|
||||
// Targets are user-specified resources to target. We need to be aware of
|
||||
// these so we don't improperly identify orphans when they've just been
|
||||
// filtered out of the graph via targeting.
|
||||
Targeting bool
|
||||
|
||||
// View, if non-nil will set a view on the module state.
|
||||
View string
|
||||
}
|
||||
@ -35,6 +41,13 @@ func (t *OrphanTransformer) Transform(g *Graph) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if t.Targeting {
|
||||
log.Printf("Skipping orphan transformer because we have targets.")
|
||||
// If we are in a run where we are targeting nodes, we won't process
|
||||
// orphans for this run.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build up all our state representatives
|
||||
resourceRep := make(map[string]struct{})
|
||||
for _, v := range g.Vertices() {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
type ResourceCountTransformer struct {
|
||||
Resource *config.Resource
|
||||
Destroy bool
|
||||
Targets []ResourceAddress
|
||||
}
|
||||
|
||||
func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||
@ -27,7 +28,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||
}
|
||||
|
||||
// For each count, build and add the node
|
||||
nodes := make([]dag.Vertex, count)
|
||||
nodes := make([]dag.Vertex, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
// Set the index. If our count is 1 we special case it so that
|
||||
// we handle the "resource.0" and "resource" boundary properly.
|
||||
@ -49,9 +50,14 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip nodes if targeting excludes them
|
||||
if !t.nodeIsTargeted(node) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the node now
|
||||
nodes[i] = node
|
||||
g.Add(nodes[i])
|
||||
nodes = append(nodes, node)
|
||||
g.Add(node)
|
||||
}
|
||||
|
||||
// Make the dependency connections
|
||||
@ -64,6 +70,25 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool {
|
||||
// no targets specified, everything stays in the graph
|
||||
if len(t.Targets) == 0 {
|
||||
return true
|
||||
}
|
||||
addressable, ok := node.(GraphNodeAddressable)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
addr := addressable.ResourceAddress()
|
||||
for _, targetAddr := range t.Targets {
|
||||
if targetAddr.Equals(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type graphNodeExpandedResource struct {
|
||||
Index int
|
||||
Resource *config.Resource
|
||||
@ -77,6 +102,23 @@ func (n *graphNodeExpandedResource) Name() string {
|
||||
return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index)
|
||||
}
|
||||
|
||||
// GraphNodeAddressable impl.
|
||||
func (n *graphNodeExpandedResource) ResourceAddress() *ResourceAddress {
|
||||
// We want this to report the logical index properly, so we must undo the
|
||||
// special case from the expand
|
||||
index := n.Index
|
||||
if index == -1 {
|
||||
index = 0
|
||||
}
|
||||
return &ResourceAddress{
|
||||
Index: index,
|
||||
// TODO: kjkjkj
|
||||
InstanceType: TypePrimary,
|
||||
Name: n.Resource.Name,
|
||||
Type: n.Resource.Type,
|
||||
}
|
||||
}
|
||||
|
||||
// GraphNodeDependable impl.
|
||||
func (n *graphNodeExpandedResource) DependableName() []string {
|
||||
return []string{
|
||||
|
103
terraform/transform_targets.go
Normal file
103
terraform/transform_targets.go
Normal file
@ -0,0 +1,103 @@
|
||||
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 {
|
||||
// TODO: duplicated in OrphanTransformer; pull up parsing earlier
|
||||
addrs, err := t.parseTargetAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetedNodes, err := t.selectTargetedNodes(g, addrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range g.Vertices() {
|
||||
if targetedNodes.Include(v) {
|
||||
} else {
|
||||
g.Remove(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, error) {
|
||||
addrs := make([]ResourceAddress, len(t.Targets))
|
||||
for i, target := range t.Targets {
|
||||
ta, err := ParseResourceAddress(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs[i] = *ta
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func (t *TargetsTransformer) selectTargetedNodes(
|
||||
g *Graph, addrs []ResourceAddress) (*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 addressable nodes
|
||||
r, ok := v.(GraphNodeAddressable)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if t.nodeIsTarget(r, addrs) {
|
||||
targetedNodes.Add(r)
|
||||
// If the node would like to know about targets, tell it.
|
||||
if n, ok := r.(GraphNodeTargetable); ok {
|
||||
n.SetTargets(addrs)
|
||||
}
|
||||
|
||||
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) nodeIsTarget(
|
||||
r GraphNodeAddressable, addrs []ResourceAddress) bool {
|
||||
addr := r.ResourceAddress()
|
||||
for _, targetAddr := range addrs {
|
||||
if targetAddr.Equals(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
71
terraform/transform_targets_test.go
Normal file
71
terraform/transform_targets_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -44,6 +44,11 @@ The command-line flags are all optional. The list of available flags are:
|
||||
* `-state-out=path` - Path to write updated state file. By default, the
|
||||
`-state` path will be used.
|
||||
|
||||
* `-target=resource` - A [Resource
|
||||
Address](/docs/internals/resource-addressing.html) 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.
|
||||
|
||||
|
@ -21,3 +21,9 @@ confirmation before destroying.
|
||||
This command accepts all the flags that the
|
||||
[apply command](/docs/commands/apply.html) accepts. If `-force` is
|
||||
set, then the destroy confirmation will not be shown.
|
||||
|
||||
The `-target` flag, instead of affecting "dependencies" will instead also
|
||||
destroy any resources that _depend on_ the target(s) specified.
|
||||
|
||||
The behavior of any `terraform destroy` command can be previewed at any time
|
||||
with an equivalent `terraform plan -destroy` command.
|
||||
|
@ -45,6 +45,11 @@ The command-line flags are all optional. The list of available flags are:
|
||||
|
||||
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
||||
|
||||
* `-target=resource` - A [Resource
|
||||
Address](/docs/internals/resource-addressing.html) 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.
|
||||
|
||||
|
@ -36,6 +36,11 @@ The command-line flags are all optional. The list of available flags are:
|
||||
* `-state-out=path` - Path to write updated state file. By default, the
|
||||
`-state` path will be used.
|
||||
|
||||
* `-target=resource` - A [Resource
|
||||
Address](/docs/internals/resource-addressing.html) 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.
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Internals: Resource Address"
|
||||
sidebar_current: "docs-internals-resource-addressing"
|
||||
description: |-
|
||||
Resource addressing is used to target specific resources in a larger
|
||||
infrastructure.
|
||||
---
|
||||
|
||||
# Resource Addressing
|
||||
|
||||
A __Resource Address__ is a string that references a specific resource in a
|
||||
larger infrastructure. The syntax of a resource address is:
|
||||
|
||||
```
|
||||
<resource_type>.<resource_name>[optional fields]
|
||||
```
|
||||
|
||||
Required fields:
|
||||
|
||||
* `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
|
||||
all instances.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
Given a Terraform config that includes:
|
||||
|
||||
```
|
||||
resource "aws_instance" "web" {
|
||||
# ...
|
||||
count = 4
|
||||
}
|
||||
```
|
||||
|
||||
An address like this:
|
||||
|
||||
|
||||
```
|
||||
aws_instance.web[3]
|
||||
```
|
||||
|
||||
Refers to only the last instance in the config, and an address like this:
|
||||
|
||||
```
|
||||
aws_instance.web
|
||||
```
|
||||
|
||||
|
||||
Refers to all four "web" instances.
|
@ -219,6 +219,10 @@
|
||||
<li<%= sidebar_current("docs-internals-lifecycle") %>>
|
||||
<a href="/docs/internals/lifecycle.html">Resource Lifecycle</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-internals-resource-addressing") %>>
|
||||
<a href="/docs/internals/resource-addressing.html">Resource Addressing</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
Loading…
Reference in New Issue
Block a user