From d94c4392eb486cb84c3c79fa92cd94e9a5ec166f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Feb 2015 11:15:54 -0800 Subject: [PATCH] terraform: validate provisioners --- terraform/context.go | 32 ++++++++++++------- terraform/context_test.go | 50 ++++++++++++++--------------- terraform/eval_context.go | 31 ++++++++++++++++++ terraform/eval_context_builtin.go | 50 ++++++++++++++++++++++++++--- terraform/eval_provisioner.go | 51 ++++++++++++++++++++++++++++++ terraform/eval_type.go | 9 +++--- terraform/eval_validate.go | 31 ++++++++++++++++++ terraform/evaltype_string.go | 4 +++ terraform/graph_builder.go | 6 ++++ terraform/graph_walk_context.go | 22 ++++++++----- terraform/transform_provisioner.go | 3 +- terraform/transform_resource.go | 27 ++++++++++------ 12 files changed, 251 insertions(+), 65 deletions(-) create mode 100644 terraform/eval_provisioner.go diff --git a/terraform/context.go b/terraform/context.go index bcbe38acbe..ebe9475b91 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -26,11 +26,12 @@ type ContextOpts struct { // perform operations on infrastructure. This structure is built using // NewContext. See the documentation for that. type Context2 struct { - module *module.Tree - providers map[string]ResourceProviderFactory - state *State - stateLock sync.RWMutex - variables map[string]string + module *module.Tree + providers map[string]ResourceProviderFactory + provisioners map[string]ResourceProvisionerFactory + state *State + stateLock sync.RWMutex + variables map[string]string } // NewContext creates a new Context structure. @@ -40,10 +41,11 @@ type Context2 struct { // the values themselves. func NewContext2(opts *ContextOpts) *Context2 { return &Context2{ - module: opts.Module, - providers: opts.Providers, - state: opts.State, - variables: opts.Variables, + module: opts.Module, + providers: opts.Providers, + provisioners: opts.Provisioners, + state: opts.State, + variables: opts.Variables, } } @@ -56,10 +58,16 @@ func (c *Context2) GraphBuilder() GraphBuilder { providers = append(providers, k) } + provisioners := make([]string, 0, len(c.provisioners)) + for k, _ := range c.provisioners { + provisioners = append(provisioners, k) + } + return &BuiltinGraphBuilder{ - Root: c.module, - Providers: providers, - State: c.state, + Root: c.module, + Providers: providers, + Provisioners: provisioners, + State: c.state, } } diff --git a/terraform/context_test.go b/terraform/context_test.go index f995785ded..7cab27d489 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -243,6 +243,31 @@ func TestContext2Validate_providerConfig_good(t *testing.T) { } } +func TestContext2Validate_provisionerConfig_bad(t *testing.T) { + m := testModule(t, "validate-bad-prov-conf") + p := testProvider("aws") + pr := testProvisioner() + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Provisioners: map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + pr.ValidateReturnErrors = []error{fmt.Errorf("bad")} + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %#v", e) + } +} + func TestContext2Validate_requiredVar(t *testing.T) { m := testModule(t, "validate-required-var") p := testProvider("aws") @@ -512,31 +537,6 @@ func TestContextValidate_tainted(t *testing.T) { } } -func TestContextValidate_provisionerConfig_bad(t *testing.T) { - m := testModule(t, "validate-bad-prov-conf") - p := testProvider("aws") - pr := testProvisioner() - c := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - Provisioners: map[string]ResourceProvisionerFactory{ - "shell": testProvisionerFuncFixed(pr), - }, - }) - - pr.ValidateReturnErrors = []error{fmt.Errorf("bad")} - - w, e := c.Validate() - if len(w) > 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) == 0 { - t.Fatalf("bad: %#v", e) - } -} - func TestContextValidate_provisionerConfig_good(t *testing.T) { m := testModule(t, "validate-bad-prov-conf") p := testProvider("aws") diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 4c8806987d..41df784c56 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -19,6 +19,16 @@ type EvalContext interface { // initialized) or returns nil if the provider isn't initialized. Provider(string) ResourceProvider + // InitProvisioner initializes the provisioner with the given name and + // returns the implementation of the resource provisioner or an error. + // + // It is an error to initialize the same provisioner more than once. + InitProvisioner(string) (ResourceProvisioner, error) + + // Provisioner gets the provisioner instance with the given name (already + // initialized) or returns nil if the provisioner isn't initialized. + Provisioner(string) ResourceProvisioner + // Interpolate takes the given raw configuration and completes // the interpolations, returning the processed ResourceConfig. // @@ -39,6 +49,15 @@ type MockEvalContext struct { ProviderName string ProviderProvider ResourceProvider + InitProvisionerCalled bool + InitProvisionerName string + InitProvisionerProvisioner ResourceProvisioner + InitProvisionerError error + + ProvisionerCalled bool + ProvisionerName string + ProvisionerProvisioner ResourceProvisioner + InterpolateCalled bool InterpolateConfig *config.RawConfig InterpolateResource *Resource @@ -61,6 +80,18 @@ func (c *MockEvalContext) Provider(n string) ResourceProvider { return c.ProviderProvider } +func (c *MockEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) { + c.InitProvisionerCalled = true + c.InitProvisionerName = n + return c.InitProvisionerProvisioner, c.InitProvisionerError +} + +func (c *MockEvalContext) Provisioner(n string) ResourceProvisioner { + c.ProvisionerCalled = true + c.ProvisionerName = n + return c.ProvisionerProvisioner +} + func (c *MockEvalContext) Interpolate( config *config.RawConfig, resource *Resource) (*ResourceConfig, error) { c.InterpolateCalled = true diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index 6fa98a30a6..f4ed0b686e 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -12,11 +12,14 @@ import ( // BuiltinEvalContext is an EvalContext implementation that is used by // Terraform by default. type BuiltinEvalContext struct { - PathValue []string - Interpolater *Interpolater - Providers map[string]ResourceProviderFactory - ProviderCache map[string]ResourceProvider - ProviderLock *sync.Mutex + PathValue []string + Interpolater *Interpolater + Providers map[string]ResourceProviderFactory + ProviderCache map[string]ResourceProvider + ProviderLock *sync.Mutex + Provisioners map[string]ResourceProvisionerFactory + ProvisionerCache map[string]ResourceProvisioner + ProvisionerLock *sync.Mutex once sync.Once } @@ -57,6 +60,43 @@ func (ctx *BuiltinEvalContext) Provider(n string) ResourceProvider { return ctx.ProviderCache[ctx.pathCacheKey()] } +func (ctx *BuiltinEvalContext) InitProvisioner( + n string) (ResourceProvisioner, error) { + ctx.once.Do(ctx.init) + + // If we already initialized, it is an error + if p := ctx.Provisioner(n); p != nil { + return nil, fmt.Errorf("Provisioner '%s' already initialized", n) + } + + // Warning: make sure to acquire these locks AFTER the call to Provisioner + // above, since it also acquires locks. + ctx.ProvisionerLock.Lock() + defer ctx.ProvisionerLock.Unlock() + + f, ok := ctx.Provisioners[n] + if !ok { + return nil, fmt.Errorf("Provisioner '%s' not found", n) + } + + p, err := f() + if err != nil { + return nil, err + } + + ctx.ProvisionerCache[ctx.pathCacheKey()] = p + return p, nil +} + +func (ctx *BuiltinEvalContext) Provisioner(n string) ResourceProvisioner { + ctx.once.Do(ctx.init) + + ctx.ProvisionerLock.Lock() + defer ctx.ProvisionerLock.Unlock() + + return ctx.ProvisionerCache[ctx.pathCacheKey()] +} + func (ctx *BuiltinEvalContext) Interpolate( cfg *config.RawConfig, r *Resource) (*ResourceConfig, error) { if cfg != nil { diff --git a/terraform/eval_provisioner.go b/terraform/eval_provisioner.go new file mode 100644 index 0000000000..8be715c82c --- /dev/null +++ b/terraform/eval_provisioner.go @@ -0,0 +1,51 @@ +package terraform + +import ( + "fmt" +) + +// EvalInitProvisioner is an EvalNode implementation that initializes a provisioner +// and returns nothing. The provisioner can be retrieved again with the +// EvalGetProvisioner node. +type EvalInitProvisioner struct { + Name string +} + +func (n *EvalInitProvisioner) Args() ([]EvalNode, []EvalType) { + return nil, nil +} + +// TODO: test +func (n *EvalInitProvisioner) Eval( + ctx EvalContext, args []interface{}) (interface{}, error) { + return ctx.InitProvisioner(n.Name) +} + +func (n *EvalInitProvisioner) Type() EvalType { + return EvalTypeNull +} + +// EvalGetProvisioner is an EvalNode implementation that retrieves an already +// initialized provisioner instance for the given name. +type EvalGetProvisioner struct { + Name string +} + +func (n *EvalGetProvisioner) Args() ([]EvalNode, []EvalType) { + return nil, nil +} + +// TODO: test +func (n *EvalGetProvisioner) Eval( + ctx EvalContext, args []interface{}) (interface{}, error) { + result := ctx.Provisioner(n.Name) + if result == nil { + return nil, fmt.Errorf("provisioner %s not initialized", n.Name) + } + + return result, nil +} + +func (n *EvalGetProvisioner) Type() EvalType { + return EvalTypeResourceProvisioner +} diff --git a/terraform/eval_type.go b/terraform/eval_type.go index ee232939ee..62c2a85153 100644 --- a/terraform/eval_type.go +++ b/terraform/eval_type.go @@ -12,8 +12,9 @@ package terraform type EvalType uint32 const ( - EvalTypeInvalid EvalType = 0 - EvalTypeNull EvalType = 1 << iota // nil - EvalTypeConfig // *ResourceConfig - EvalTypeResourceProvider // ResourceProvider + EvalTypeInvalid EvalType = 0 + EvalTypeNull EvalType = 1 << iota // nil + EvalTypeConfig // *ResourceConfig + EvalTypeResourceProvider // ResourceProvider + EvalTypeResourceProvisioner // ResourceProvisioner ) diff --git a/terraform/eval_validate.go b/terraform/eval_validate.go index 533e48e384..776786da3f 100644 --- a/terraform/eval_validate.go +++ b/terraform/eval_validate.go @@ -91,6 +91,37 @@ func (n *EvalValidateProvider) Type() EvalType { return EvalTypeNull } +// EvalValidateProvisioner is an EvalNode implementation that validates +// the configuration of a resource. +type EvalValidateProvisioner struct { + Provisioner EvalNode + Config EvalNode +} + +func (n *EvalValidateProvisioner) Args() ([]EvalNode, []EvalType) { + return []EvalNode{n.Provisioner, n.Config}, + []EvalType{EvalTypeResourceProvisioner, EvalTypeConfig} +} + +func (n *EvalValidateProvisioner) Eval( + ctx EvalContext, args []interface{}) (interface{}, error) { + provider := args[0].(ResourceProvisioner) + config := args[1].(*ResourceConfig) + warns, errs := provider.Validate(config) + if len(warns) == 0 && len(errs) == 0 { + return nil, nil + } + + return nil, &EvalValidateError{ + Warnings: warns, + Errors: errs, + } +} + +func (n *EvalValidateProvisioner) Type() EvalType { + return EvalTypeNull +} + // EvalValidateResource is an EvalNode implementation that validates // the configuration of a resource. type EvalValidateResource struct { diff --git a/terraform/evaltype_string.go b/terraform/evaltype_string.go index 6e60fd28dd..d2efcabde3 100644 --- a/terraform/evaltype_string.go +++ b/terraform/evaltype_string.go @@ -9,6 +9,7 @@ const ( _EvalType_name_1 = "EvalTypeNull" _EvalType_name_2 = "EvalTypeConfig" _EvalType_name_3 = "EvalTypeResourceProvider" + _EvalType_name_4 = "EvalTypeResourceProvisioner" ) var ( @@ -16,6 +17,7 @@ var ( _EvalType_index_1 = [...]uint8{0, 12} _EvalType_index_2 = [...]uint8{0, 14} _EvalType_index_3 = [...]uint8{0, 24} + _EvalType_index_4 = [...]uint8{0, 27} ) func (i EvalType) String() string { @@ -28,6 +30,8 @@ func (i EvalType) String() string { return _EvalType_name_2 case i == 8: return _EvalType_name_3 + case i == 16: + return _EvalType_name_4 default: return fmt.Sprintf("EvalType(%d)", i) } diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index 4299681832..5ff371ec65 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -52,6 +52,9 @@ type BuiltinGraphBuilder struct { // Providers is the list of providers supported. Providers []string + + // Provisioners is the list of provisioners supported. + Provisioners []string } // Build builds the graph according to the steps returned by Steps. @@ -78,6 +81,9 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { &PruneProviderTransformer{}, // Provisioner-related transformations + &MissingProvisionerTransformer{Provisioners: b.Provisioners}, + &ProvisionerTransformer{}, + &PruneProvisionerTransformer{}, // Run our vertex-level transforms &VertexTransformer{ diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index 8f7e2f8f56..deef3ce6d8 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -22,20 +22,25 @@ type ContextGraphWalker struct { ValidationWarnings []string ValidationErrors []error - errorLock sync.Mutex - once sync.Once - providerCache map[string]ResourceProvider - providerLock sync.Mutex + errorLock sync.Mutex + once sync.Once + providerCache map[string]ResourceProvider + providerLock sync.Mutex + provisionerCache map[string]ResourceProvisioner + provisionerLock sync.Mutex } func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext { w.once.Do(w.init) return &BuiltinEvalContext{ - PathValue: g.Path, - Providers: w.Context.providers, - ProviderCache: w.providerCache, - ProviderLock: &w.providerLock, + PathValue: g.Path, + Providers: w.Context.providers, + ProviderCache: w.providerCache, + ProviderLock: &w.providerLock, + Provisioners: w.Context.provisioners, + ProvisionerCache: w.provisionerCache, + ProvisionerLock: &w.provisionerLock, Interpolater: &Interpolater{ Operation: w.Operation, Module: w.Context.module, @@ -72,4 +77,5 @@ func (w *ContextGraphWalker) ExitEvalTree( func (w *ContextGraphWalker) init() { w.providerCache = make(map[string]ResourceProvider, 5) + w.provisionerCache = make(map[string]ResourceProvisioner, 5) } diff --git a/terraform/transform_provisioner.go b/terraform/transform_provisioner.go index 049123d009..316a8f31e9 100644 --- a/terraform/transform_provisioner.go +++ b/terraform/transform_provisioner.go @@ -104,8 +104,7 @@ func (n *graphNodeMissingProvisioner) Name() string { // GraphNodeEvalable impl. func (n *graphNodeMissingProvisioner) EvalTree() EvalNode { - return nil - //return ProvisionerEvalTree(n.ProvisionerNameValue, nil) + return &EvalInitProvisioner{Name: n.ProvisionerNameValue} } func (n *graphNodeMissingProvisioner) ProvisionerName() string { diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index e5465c1f2c..1178cab6db 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -78,14 +78,23 @@ func (n *graphNodeExpandedResource) ProvidedBy() string { // GraphNodeEvalable impl. func (n *graphNodeExpandedResource) EvalTree() EvalNode { - return &EvalSequence{ - Nodes: []EvalNode{ - &EvalValidateResource{ - Provider: &EvalGetProvider{Name: n.ProvidedBy()}, - Config: &EvalInterpolate{Config: n.Resource.RawConfig}, - ResourceName: n.Resource.Name, - ResourceType: n.Resource.Type, - }, - }, + seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} + + // Validate the resource + seq.Nodes = append(seq.Nodes, &EvalValidateResource{ + Provider: &EvalGetProvider{Name: n.ProvidedBy()}, + Config: &EvalInterpolate{Config: n.Resource.RawConfig}, + ResourceName: n.Resource.Name, + ResourceType: n.Resource.Type, + }) + + // Validate all the provisioners + for _, p := range n.Resource.Provisioners { + seq.Nodes = append(seq.Nodes, &EvalValidateProvisioner{ + Provisioner: &EvalGetProvisioner{Name: p.Type}, + Config: &EvalInterpolate{Config: p.RawConfig}, + }) } + + return seq }