Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: Refresh, Validate, Input on new graph builders #11426

Merged
merged 9 commits into from
Jan 26, 2017
49 changes: 43 additions & 6 deletions terraform/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,35 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, error) {
Validate: opts.Validate,
}).Build(RootModulePath)

case GraphTypeInput:
// The input graph is just a slightly modified plan graph
fallthrough
case GraphTypeValidate:
// The validate graph is just a slightly modified plan graph
fallthrough
case GraphTypePlan:
return (&PlanGraphBuilder{
// Create the plan graph builder
p := &PlanGraphBuilder{
Module: c.module,
State: c.state,
Providers: c.components.ResourceProviders(),
Targets: c.targets,
Validate: opts.Validate,
}).Build(RootModulePath)
}

// Some special cases for other graph types shared with plan currently
var b GraphBuilder = p
switch typ {
case GraphTypeInput:
b = InputGraphBuilder(p)
case GraphTypeValidate:
// We need to set the provisioners so those can be validated
p.Provisioners = c.components.ResourceProvisioners()

b = ValidateGraphBuilder(p)
}

return b.Build(RootModulePath)

case GraphTypePlanDestroy:
return (&DestroyPlanGraphBuilder{
Expand All @@ -233,6 +254,15 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, error) {
Validate: opts.Validate,
}).Build(RootModulePath)

case GraphTypeRefresh:
return (&RefreshGraphBuilder{
Module: c.module,
State: c.state,
Providers: c.components.ResourceProviders(),
Targets: c.targets,
Validate: opts.Validate,
}).Build(RootModulePath)

case GraphTypeLegacy:
return c.graphBuilder(opts).Build(RootModulePath)
}
Expand Down Expand Up @@ -402,7 +432,7 @@ func (c *Context) Input(mode InputMode) error {

if mode&InputModeProvider != 0 {
// Build the graph
graph, err := c.Graph(GraphTypeLegacy, nil)
graph, err := c.Graph(GraphTypeInput, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -569,8 +599,15 @@ func (c *Context) Refresh() (*State, error) {
// Copy our own state
c.state = c.state.DeepCopy()

// Build the graph
graph, err := c.Graph(GraphTypeLegacy, nil)
// Used throughout below
X_legacyGraph := experiment.Enabled(experiment.X_legacyGraph)

// Build the graph.
graphType := GraphTypeLegacy
if !X_legacyGraph {
graphType = GraphTypeRefresh
}
graph, err := c.Graph(graphType, nil)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -641,7 +678,7 @@ func (c *Context) Validate() ([]string, []error) {
// We also validate the graph generated here, but this graph doesn't
// necessarily match the graph that Plan will generate, so we'll validate the
// graph again later after Planning.
graph, err := c.Graph(GraphTypeLegacy, nil)
graph, err := c.Graph(GraphTypeValidate, nil)
if err != nil {
return nil, []error{err}
}
Expand Down
6 changes: 6 additions & 0 deletions terraform/context_graph_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,23 @@ type GraphType byte
const (
GraphTypeInvalid GraphType = 0
GraphTypeLegacy GraphType = iota
GraphTypeRefresh
GraphTypePlan
GraphTypePlanDestroy
GraphTypeApply
GraphTypeInput
GraphTypeValidate
)

// GraphTypeMap is a mapping of human-readable string to GraphType. This
// is useful to use as the mechanism for human input for configurable
// graph types.
var GraphTypeMap = map[string]GraphType{
"apply": GraphTypeApply,
"input": GraphTypeInput,
"plan": GraphTypePlan,
"plan-destroy": GraphTypePlanDestroy,
"refresh": GraphTypeRefresh,
"legacy": GraphTypeLegacy,
"validate": GraphTypeValidate,
}
18 changes: 17 additions & 1 deletion terraform/context_refresh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ func TestContext2Refresh_outputPartial(t *testing.T) {
}
}

func TestContext2Refresh_state(t *testing.T) {
func TestContext2Refresh_stateBasic(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "refresh-basic")
state := &State{
Expand All @@ -529,6 +529,7 @@ func TestContext2Refresh_state(t *testing.T) {
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
Expand Down Expand Up @@ -737,6 +738,21 @@ func TestContext2Refresh_unknownProvider(t *testing.T) {
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{},
State: &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
})

if _, err := ctx.Refresh(); err == nil {
Expand Down
74 changes: 74 additions & 0 deletions terraform/eval_validate_selfref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package terraform

import (
"fmt"

"github.com/hashicorp/terraform/config"
)

// EvalValidateResourceSelfRef is an EvalNode implementation that validates that
// a configuration doesn't contain a reference to the resource itself.
//
// This must be done prior to interpolating configuration in order to avoid
// any infinite loop scenarios.
type EvalValidateResourceSelfRef struct {
Addr **ResourceAddress
Config **config.RawConfig
}

func (n *EvalValidateResourceSelfRef) Eval(ctx EvalContext) (interface{}, error) {
addr := *n.Addr
conf := *n.Config

// Go through the variables and find self references
var errs []error
for k, raw := range conf.Variables {
rv, ok := raw.(*config.ResourceVariable)
if !ok {
continue
}

// Build an address from the variable
varAddr := &ResourceAddress{
Path: addr.Path,
Mode: rv.Mode,
Type: rv.Type,
Name: rv.Name,
Index: rv.Index,
InstanceType: TypePrimary,
}

// If the variable access is a multi-access (*), then we just
// match the index so that we'll match our own addr if everything
// else matches.
if rv.Multi && rv.Index == -1 {
varAddr.Index = addr.Index
}

// This is a weird thing where ResourceAddres has index "-1" when
// index isn't set at all. This means index "0" for resource access.
// So, if we have this scenario, just set our varAddr to -1 so it
// matches.
if addr.Index == -1 && varAddr.Index == 0 {
varAddr.Index = -1
}

// If the addresses match, then this is a self reference
if varAddr.Equals(addr) && varAddr.Index == addr.Index {
errs = append(errs, fmt.Errorf(
"%s: self reference not allowed: %q",
addr, k))
}
}

// If no errors, no errors!
if len(errs) == 0 {
return nil, nil
}

// Wrap the errors in the proper wrapper so we can handle validation
// formatting properly upstream.
return nil, &EvalValidateError{
Errors: errs,
}
}
99 changes: 99 additions & 0 deletions terraform/eval_validate_selfref_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package terraform

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/config"
)

func TestEvalValidateResourceSelfRef(t *testing.T) {
cases := []struct {
Name string
Addr string
Config map[string]interface{}
Err bool
}{
{
"no interpolations",
"aws_instance.foo",
map[string]interface{}{
"foo": "bar",
},
false,
},

{
"non self reference",
"aws_instance.foo",
map[string]interface{}{
"foo": "${aws_instance.bar.id}",
},
false,
},

{
"self reference",
"aws_instance.foo",
map[string]interface{}{
"foo": "hello ${aws_instance.foo.id}",
},
true,
},

{
"self reference other index",
"aws_instance.foo",
map[string]interface{}{
"foo": "hello ${aws_instance.foo.4.id}",
},
false,
},

{
"self reference same index",
"aws_instance.foo[4]",
map[string]interface{}{
"foo": "hello ${aws_instance.foo.4.id}",
},
true,
},

{
"self reference multi",
"aws_instance.foo[4]",
map[string]interface{}{
"foo": "hello ${aws_instance.foo.*.id}",
},
true,
},

{
"self reference multi single",
"aws_instance.foo",
map[string]interface{}{
"foo": "hello ${aws_instance.foo.*.id}",
},
true,
},
}

for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
addr, err := ParseResourceAddress(tc.Addr)
if err != nil {
t.Fatalf("err: %s", err)
}
conf := config.TestRawConfig(t, tc.Config)

n := &EvalValidateResourceSelfRef{Addr: &addr, Config: &conf}
result, err := n.Eval(nil)
if result != nil {
t.Fatal("result should always be nil")
}
if (err != nil) != tc.Err {
t.Fatalf("err: %s", err)
}
})
}
}
27 changes: 27 additions & 0 deletions terraform/graph_builder_input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package terraform

import (
"github.com/hashicorp/terraform/dag"
)

// InputGraphBuilder creates the graph for the input operation.
//
// Unlike other graph builders, this is a function since it currently modifies
// and is based on the PlanGraphBuilder. The PlanGraphBuilder passed in will be
// modified and should not be used for any other operations.
func InputGraphBuilder(p *PlanGraphBuilder) GraphBuilder {
// We're going to customize the concrete functions
p.CustomConcrete = true

// Set the provider to the normal provider. This will ask for input.
p.ConcreteProvider = func(a *NodeAbstractProvider) dag.Vertex {
return &NodeApplyableProvider{
NodeAbstractProvider: a,
}
}

// We purposely don't set any more concrete fields since the remainder
// should be no-ops.

return p
}
Loading