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.
This commit is contained in:
Martin Atkins 2018-06-22 13:04:19 -07:00
parent eac8779870
commit fd371d838d
11 changed files with 99 additions and 43 deletions

View File

@ -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.

View File

@ -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() {

View File

@ -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.

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()

View File

@ -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{}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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{