From fd371d838d72875ab83da57d817362b7bf4c9bb6 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 22 Jun 2018 13:04:19 -0700 Subject: [PATCH] core: Handle count.index evaluation more explicitly Previously we had the evaluate methods accept directly an addrs.InstanceKey and had our evaluator infer a suitable value for count.index for it, but that prevents us from setting the index to be unknown in the validation scenario where we may not be able to predict the number of instances yet but we still want to be able to check that the configuration block is type-safe for all possible count values. To achieve this, we separate the concern of deciding on a value for count.index from the concern of evaluating it, which then allows for other implementations of this in future. For the purpose of this commit there is no change in behavior, with the count.index value being populated whenever the instance key is a number. This commit does a little more groundwork for the future implementation of the for_each feature (which'll support each.key and each.value) but still doesn't yet implement it, leaving it just stubbed out for the moment. --- terraform/context.go | 2 +- terraform/eval_apply.go | 6 ++- terraform/eval_context.go | 4 +- terraform/eval_context_builtin.go | 14 +++---- terraform/eval_context_mock.go | 24 ++++++------ terraform/eval_diff.go | 3 +- terraform/eval_lang.go | 2 +- terraform/eval_provider.go | 2 +- terraform/eval_read_data.go | 4 +- terraform/eval_validate.go | 18 ++++++--- terraform/evaluate.go | 63 ++++++++++++++++++++++++++----- 11 files changed, 99 insertions(+), 43 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index a91c05325a..2dbbc4663f 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -416,7 +416,7 @@ func (c *Context) Eval(path addrs.ModuleInstance) (*lang.Scope, tfdiags.Diagnost // caches its contexts, so we should get hold of the context that was // previously used for evaluation here, unless we skipped walking. evalCtx := walker.EnterPath(path) - return evalCtx.EvaluationScope(nil, addrs.NoKey), diags + return evalCtx.EvaluationScope(nil, EvalDataForNoInstanceKey), diags } // Interpolater is no longer used. Use Evaluator instead. diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index 6bd1e611b1..4108ab9def 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -313,13 +313,15 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*configs.Provisio provisioner := ctx.Provisioner(prov.Type) schema := ctx.ProvisionerSchema(prov.Type) + keyData := EvalDataForInstanceKey(instanceAddr.Key) + // Evaluate the main provisioner configuration. - config, _, configDiags := ctx.EvaluateBlock(prov.Config, schema, instanceAddr, instanceAddr.Key) + config, _, configDiags := ctx.EvaluateBlock(prov.Config, schema, instanceAddr, keyData) diags = diags.Append(configDiags) // A provisioner may not have a connection block if prov.Connection != nil { - connInfo, _, connInfoDiags := ctx.EvaluateBlock(prov.Connection.Config, connectionBlockSupersetSchema, instanceAddr, instanceAddr.Key) + connInfo, _, connInfoDiags := ctx.EvaluateBlock(prov.Connection.Config, connectionBlockSupersetSchema, instanceAddr, keyData) diags = diags.Append(connInfoDiags) if configDiags.HasErrors() || connInfoDiags.HasErrors() { diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 8f0a10e2fe..660d6bb79e 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -100,7 +100,7 @@ type EvalContext interface { // "dynamic" blocks replaced with zero or more static blocks. This can be // used to extract correct source location information about attributes of // the returned object value. - EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, key addrs.InstanceKey) (cty.Value, hcl.Body, tfdiags.Diagnostics) + EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) // EvaluateExpr takes the given HCL expression and evaluates it to produce // a value. @@ -112,7 +112,7 @@ type EvalContext interface { // EvaluationScope returns a scope that can be used to evaluate reference // addresses in this context. - EvaluationScope(self addrs.Referenceable, key addrs.InstanceKey) *lang.Scope + EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope // SetModuleCallArguments defines values for the variables of a particular // child module call. diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index 4d0cc590b5..cbbe9b377d 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -271,9 +271,9 @@ func (ctx *BuiltinEvalContext) CloseProvisioner(n string) error { return nil } -func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, key addrs.InstanceKey) (cty.Value, hcl.Body, tfdiags.Diagnostics) { +func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - scope := ctx.EvaluationScope(self, key) + scope := ctx.EvaluationScope(self, keyData) body, evalDiags := scope.ExpandBlock(body, schema) diags = diags.Append(evalDiags) val, evalDiags := scope.EvalBlock(body, schema) @@ -282,15 +282,15 @@ func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema } func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) { - scope := ctx.EvaluationScope(self, addrs.NoKey) + scope := ctx.EvaluationScope(self, EvalDataForNoInstanceKey) return scope.EvalExpr(expr, wantType) } -func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, key addrs.InstanceKey) *lang.Scope { +func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { data := &evaluationStateData{ - Evaluator: ctx.Evaluator, - ModulePath: ctx.PathValue, - InstanceKey: key, + Evaluator: ctx.Evaluator, + ModulePath: ctx.PathValue, + InstanceKeyData: keyData, } return ctx.Evaluator.Scope(data, self) } diff --git a/terraform/eval_context_mock.go b/terraform/eval_context_mock.go index 49dbc688d0..c31ad62a0e 100644 --- a/terraform/eval_context_mock.go +++ b/terraform/eval_context_mock.go @@ -80,12 +80,12 @@ type MockEvalContext struct { EvaluateBlockBody hcl.Body EvaluateBlockSchema *configschema.Block EvaluateBlockSelf addrs.Referenceable - EvaluateBlockKey addrs.InstanceKey + EvaluateBlockKeyData InstanceKeyEvalData EvaluateBlockResultFunc func( body hcl.Body, schema *configschema.Block, self addrs.Referenceable, - key addrs.InstanceKey, + keyData InstanceKeyEvalData, ) (cty.Value, hcl.Body, tfdiags.Diagnostics) // overrides the other values below, if set EvaluateBlockResult cty.Value EvaluateBlockExpandedBody hcl.Body @@ -103,10 +103,10 @@ type MockEvalContext struct { EvaluateExprResult cty.Value EvaluateExprDiags tfdiags.Diagnostics - EvaluationScopeCalled bool - EvaluationScopeSelf addrs.Referenceable - EvaluationScopeKey addrs.InstanceKey - EvaluationScopeScope *lang.Scope + EvaluationScopeCalled bool + EvaluationScopeSelf addrs.Referenceable + EvaluationScopeKeyData InstanceKeyEvalData + EvaluationScopeScope *lang.Scope InterpolateCalled bool InterpolateConfig *config.RawConfig @@ -228,14 +228,14 @@ func (c *MockEvalContext) CloseProvisioner(n string) error { return nil } -func (c *MockEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, key addrs.InstanceKey) (cty.Value, hcl.Body, tfdiags.Diagnostics) { +func (c *MockEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) { c.EvaluateBlockCalled = true c.EvaluateBlockBody = body c.EvaluateBlockSchema = schema c.EvaluateBlockSelf = self - c.EvaluateBlockKey = key + c.EvaluateBlockKeyData = keyData if c.EvaluateBlockResultFunc != nil { - return c.EvaluateBlockResultFunc(body, schema, self, key) + return c.EvaluateBlockResultFunc(body, schema, self, keyData) } return c.EvaluateBlockResult, c.EvaluateBlockExpandedBody, c.EvaluateBlockDiags } @@ -261,7 +261,7 @@ func (c *MockEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, s // This function overwrites any existing functions installed in fields // EvaluateBlockResultFunc and EvaluateExprResultFunc. func (c *MockEvalContext) installSimpleEval() { - c.EvaluateBlockResultFunc = func(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, key addrs.InstanceKey) (cty.Value, hcl.Body, tfdiags.Diagnostics) { + c.EvaluateBlockResultFunc = func(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) { if scope := c.EvaluationScopeScope; scope != nil { // Fully-functional codepath. var diags tfdiags.Diagnostics @@ -304,10 +304,10 @@ func (c *MockEvalContext) installSimpleEval() { } } -func (c *MockEvalContext) EvaluationScope(self addrs.Referenceable, key addrs.InstanceKey) *lang.Scope { +func (c *MockEvalContext) EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { c.EvaluationScopeCalled = true c.EvaluationScopeSelf = self - c.EvaluationScopeKey = key + c.EvaluationScopeKeyData = keyData return c.EvaluationScopeScope } diff --git a/terraform/eval_diff.go b/terraform/eval_diff.go index 891e7a3d69..ced760df29 100644 --- a/terraform/eval_diff.go +++ b/terraform/eval_diff.go @@ -133,7 +133,8 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) { // Should be caught during validation, so we don't bother with a pretty error here return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) } - configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, n.Addr.Key) + keyData := EvalDataForInstanceKey(n.Addr.Key) + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, diags.Err() diff --git a/terraform/eval_lang.go b/terraform/eval_lang.go index d309622ed9..f99e4c4bfa 100644 --- a/terraform/eval_lang.go +++ b/terraform/eval_lang.go @@ -26,7 +26,7 @@ type EvalConfigBlock struct { } func (n *EvalConfigBlock) Eval(ctx EvalContext) (interface{}, error) { - val, body, diags := ctx.EvaluateBlock(*n.Config, n.Schema, n.SelfAddr, addrs.NoKey) + val, body, diags := ctx.EvaluateBlock(*n.Config, n.Schema, n.SelfAddr, EvalDataForNoInstanceKey) if diags.HasErrors() && n.ContinueOnErr { log.Printf("[WARN] Block evaluation failed: %s", diags.Err()) return nil, EvalEarlyExitError{} diff --git a/terraform/eval_provider.go b/terraform/eval_provider.go index be695b2f5b..59d5d3b42a 100644 --- a/terraform/eval_provider.go +++ b/terraform/eval_provider.go @@ -74,7 +74,7 @@ func (n *EvalConfigProvider) Eval(ctx EvalContext) (interface{}, error) { } configSchema := schema.Provider - configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, addrs.NoKey) + configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey) diags = diags.Append(evalDiags) if evalDiags.HasErrors() { return nil, diags.NonFatalErr() diff --git a/terraform/eval_read_data.go b/terraform/eval_read_data.go index ec50bfe82f..c0c37d0e09 100644 --- a/terraform/eval_read_data.go +++ b/terraform/eval_read_data.go @@ -63,8 +63,10 @@ func (n *EvalReadDataDiff) Eval(ctx EvalContext) (interface{}, error) { return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type) } + keyData := EvalDataForInstanceKey(n.Addr.Key) + var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, n.Addr.Key) + configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, diags.Err() diff --git a/terraform/eval_validate.go b/terraform/eval_validate.go index 88712af978..b7b73e7416 100644 --- a/terraform/eval_validate.go +++ b/terraform/eval_validate.go @@ -92,7 +92,7 @@ func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) { configSchema = &configschema.Block{} } - configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, addrs.NoKey) + configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey) diags = diags.Append(evalDiags) if evalDiags.HasErrors() { return nil, diags.NonFatalErr() @@ -142,7 +142,9 @@ func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) { { // Validate the provisioner's own config first - configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, n.ResourceAddr, n.ResourceAddr.Key) + keyData := EvalDataForInstanceKey(n.ResourceAddr.Key) + + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, n.ResourceAddr, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, diags.Err() @@ -197,10 +199,12 @@ func (n *EvalValidateProvisioner) validateConnConfig(ctx EvalContext, config *co return diags } + keyData := EvalDataForInstanceKey(n.ResourceAddr.Key) + // We evaluate here just by evaluating the block and returning any // diagnostics we get, since evaluation alone is enough to check for // extraneous arguments and incorrectly-typed arguments. - _, _, configDiags := ctx.EvaluateBlock(config.Config, connectionBlockSupersetSchema, self, n.ResourceAddr.Key) + _, _, configDiags := ctx.EvaluateBlock(config.Config, connectionBlockSupersetSchema, self, keyData) diags = diags.Append(configDiags) return diags @@ -361,7 +365,9 @@ func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) { return nil, diags.Err() } - configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, n.Addr.Key) + keyData := EvalDataForInstanceKey(n.Addr.Key) + + configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return nil, diags.Err() @@ -388,7 +394,9 @@ func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) { return nil, diags.Err() } - configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, n.Addr.Key) + keyData := EvalDataForInstanceKey(n.Addr.Key) + + configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return nil, diags.Err() diff --git a/terraform/evaluate.go b/terraform/evaluate.go index ce622b53cf..32064b8d67 100644 --- a/terraform/evaluate.go +++ b/terraform/evaluate.go @@ -83,11 +83,58 @@ type evaluationStateData struct { // that references will be resolved relative to. ModulePath addrs.ModuleInstance - // InstanceKey is the instance key for the object being evaluated, if any. - // Set to addrs.NoKey if no object repetition is in progress. - InstanceKey addrs.InstanceKey + // InstanceKeyData describes the values, if any, that are accessible due + // to repetition of a containing object using "count" or "for_each" + // arguments. (It is _not_ used for the for_each inside "dynamic" blocks, + // since the user specifies in that case which variable name to locally + // shadow.) + InstanceKeyData InstanceKeyEvalData } +// InstanceKeyEvalData is used during evaluation to specify which values, +// if any, should be produced for count.index, each.key, and each.value. +type InstanceKeyEvalData struct { + // CountIndex is the value for count.index, or cty.NilVal if evaluating + // in a context where the "count" argument is not active. + // + // For correct operation, this should always be of type cty.Number if not + // nil. + CountIndex cty.Value + + // EachKey and EachValue are the values for each.key and each.value + // respectively, or cty.NilVal if evaluating in a context where the + // "for_each" argument is not active. These must either both be set + // or neither set. + // + // For correct operation, EachKey must always be either of type cty.String + // or cty.Number if not nil. + EachKey, EachValue cty.Value +} + +// EvalDataForInstanceKey constructs a suitable InstanceKeyEvalData for +// evaluating in a context that has the given instance key. +func EvalDataForInstanceKey(key addrs.InstanceKey) InstanceKeyEvalData { + // At the moment we don't actually implement for_each, so we only + // ever populate CountIndex. + // (When we implement for_each later we may need to reorganize this some, + // so that we can resolve the ambiguity that an int key may either be + // a count.index or an each.key where for_each is over a list.) + + var countIdx cty.Value + if intKey, ok := key.(addrs.IntKey); ok { + countIdx = cty.NumberIntVal(int64(intKey)) + } + + return InstanceKeyEvalData{ + CountIndex: countIdx, + } +} + +// EvalDataForNoInstanceKey is a value of InstanceKeyData that sets no instance +// key values at all, suitable for use in contexts where no keyed instance +// is relevant. +var EvalDataForNoInstanceKey = InstanceKeyEvalData{} + // evaluationStateData must implement lang.Data var _ lang.Data = (*evaluationStateData)(nil) @@ -96,12 +143,8 @@ func (d *evaluationStateData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.Sou switch addr.Name { case "index": - key := d.InstanceKey - // key might not be set at all (addrs.NoKey) or it might be a string - // if we're actually in a for_each block, so we'll check first and - // produce a nice error if this is being used in the wrong context. - intKey, ok := key.(addrs.IntKey) - if !ok { + idxVal := d.InstanceKeyData.CountIndex + if idxVal == cty.NilVal { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: `Reference to "count" in non-counted context`, @@ -110,7 +153,7 @@ func (d *evaluationStateData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.Sou }) return cty.UnknownVal(cty.Number), diags } - return cty.NumberIntVal(int64(intKey)), diags + return idxVal, diags default: diags = diags.Append(&hcl.Diagnostic{