From 630e3b4be2ee47d30a7149c0703554cbbe82e977 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 3 May 2018 18:28:12 -0700 Subject: [PATCH] core: evaluation for HCL2-flavored reference expressions Here we replace the stub implementations in evaluationStateData with real implementations that are based on their equivalents in the old Interpolator type. The behavior here is a little different due to the different semantics expected under HCL2, but the principle remains the same: the main references are resolved from the state, using config for validation in order to produce some helpful error messages. --- terraform/evaluate.go | 629 ++++++++++++++++++++++++++++++++++++- terraform/evaluate_test.go | 33 ++ 2 files changed, 645 insertions(+), 17 deletions(-) create mode 100644 terraform/evaluate_test.go diff --git a/terraform/evaluate.go b/terraform/evaluate.go index a23abbec1..483b153ac 100644 --- a/terraform/evaluate.go +++ b/terraform/evaluate.go @@ -1,14 +1,23 @@ package terraform import ( + "fmt" + "os" + "strconv" + "strings" "sync" - "github.com/hashicorp/terraform/tfdiags" + "github.com/agext/levenshtein" + "github.com/hashicorp/hcl2/hcl" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/config/configschema" + "github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/lang" + "github.com/hashicorp/terraform/tfdiags" ) // Evaluator provides the necessary contextual data for evaluating expressions @@ -34,6 +43,12 @@ type Evaluator struct { VariableValues map[string]map[string]cty.Value VariableValuesLock *sync.Mutex + // ProviderSchemas is a map of schemas for all provider configurations + // that have been initialized so far. This is mutated concurrently, so + // it must be accessed only while holding ProvidersLock. + ProviderSchemas map[string]*ProviderSchema + ProvidersLock *sync.Mutex + // State is the current state. During some operations this structure // is mutated concurrently, and so it must be accessed only while holding // StateLock. @@ -64,39 +79,619 @@ type evaluationStateData struct { // ModulePath is the path through the dynamic module tree to the module // 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 } // evaluationStateData must implement lang.Data var _ lang.Data = (*evaluationStateData)(nil) -func (d *evaluationStateData) GetCountAttr(addrs.CountAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + 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 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to "count" in non-counted context`, + Detail: fmt.Sprintf(`The "count" object can be used only in "resource" and "data" blocks, and only when the "count" argument is set.`), + Subject: rng.ToHCL().Ptr(), + }) + return cty.UnknownVal(cty.Number), diags + } + return cty.NumberIntVal(int64(intKey)), diags + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "count" attribute`, + Detail: fmt.Sprintf(`The "count" object does not have an attribute named %q. The only supported attribute is count.index, which is the index of each instance of a resource block that has the "count" argument set.`, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } } -func (d *evaluationStateData) GetInputVariable(addrs.InputVariable, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // First we'll make sure the requested value is declared in configuration, + // so we can produce a nice message if not. + moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("input variable read from %s, which has no configuration", d.ModulePath)) + } + + config := moduleConfig.Module.Variables[addr.Name] + if config == nil { + var suggestions []string + for k := range moduleConfig.Module.Variables { + suggestions = append(suggestions, k) + } + suggestion := nameSuggestion(addr.Name, suggestions) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } else { + suggestion = fmt.Sprintf(" This variable can be declared with a variable %q {} block.", addr.Name) + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to undeclared input variable`, + Detail: fmt.Sprintf(`An input variable with the name %q has not been declared.%s`, addr.Name, suggestion), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + wantType := cty.DynamicPseudoType + if config.Type != cty.NilType { + wantType = config.Type + } + + d.Evaluator.VariableValuesLock.Lock() + defer d.Evaluator.VariableValuesLock.Unlock() + + moduleAddrStr := d.ModulePath.String() + vals := d.Evaluator.VariableValues[moduleAddrStr] + if vals == nil { + return cty.UnknownVal(wantType), diags + } + + val, isSet := vals[addr.Name] + if !isSet { + if config.Default != cty.NilVal { + return config.Default, diags + } + return cty.UnknownVal(wantType), diags + } + + var err error + val, err = convert.Convert(val, wantType) + if err != nil { + // We should never get here because this problem should've been caught + // during earlier validation, but we'll do something reasonable anyway. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Incorrect variable type`, + Detail: fmt.Sprintf(`The resolved value of variable %q is not appropriate: %s.`, addr.Name, err), + Subject: &config.DeclRange, + }) + // Stub out our return value so that the semantic checker doesn't + // produce redundant downstream errors. + val = cty.UnknownVal(wantType) + } + + return val, diags } -func (d *evaluationStateData) GetLocalValue(addrs.LocalValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // First we'll make sure the requested value is declared in configuration, + // so we can produce a nice message if not. + moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("local value read from %s, which has no configuration", d.ModulePath)) + } + + config := moduleConfig.Module.Locals[addr.Name] + if config == nil { + var suggestions []string + for k := range moduleConfig.Module.Locals { + suggestions = append(suggestions, k) + } + suggestion := nameSuggestion(addr.Name, suggestions) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to undeclared local value`, + Detail: fmt.Sprintf(`A local value with the name %q has not been declared.%s`, addr.Name, suggestion), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + // Now we'll retrieve the value from the state, which means we need to hold + // the state lock. + d.Evaluator.StateLock.RLock() + defer d.Evaluator.StateLock.RUnlock() + + ms := d.Evaluator.State.ModuleByPath(d.ModulePath) + if ms == nil { + // Not evaluated yet? + return cty.DynamicVal, diags + } + + rawV, exists := ms.Locals[addr.Name] + if !exists { + // Not evaluated yet? + return cty.DynamicVal, diags + } + + // The state structures haven't yet been updated to the new type system, + // so we'll need to shim here. + // FIXME: Remove this once ms.Locals is itself a map[string]cty.Value. + val := hcl2shim.HCL2ValueFromConfigValue(rawV) + + return val, diags } -func (d *evaluationStateData) GetModuleInstance(addrs.ModuleCallInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) GetModuleInstance(addr addrs.ModuleCallInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // Output results live in the module that declares them, which is one of + // the child module instances of our current module path. + moduleAddr := addr.ModuleInstance(d.ModulePath) + + // We'll consult the configuration to see what output names we are + // expecting, so we can ensure the resulting object is of the expected + // type even if our data is incomplete for some reason. + moduleConfig := d.Evaluator.Config.DescendentForInstance(moduleAddr) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("output value read from %s, which has no configuration", moduleAddr)) + } + outputConfigs := moduleConfig.Module.Outputs + + // Now we'll retrieve the values from the state, which means we need to hold + // the state lock. + d.Evaluator.StateLock.RLock() + defer d.Evaluator.StateLock.RUnlock() + + ms := d.Evaluator.State.ModuleByPath(moduleAddr) + if ms == nil { + // Not evaluated yet? + // We'll return an unknown value of a suitable object type so that we + // can still detect attempts to access outputs that aren't defined. + attrs := map[string]cty.Type{} + for name := range outputConfigs { + attrs[name] = cty.DynamicPseudoType + } + return cty.UnknownVal(cty.Object(attrs)), diags + } + + vals := map[string]cty.Value{} + for name := range outputConfigs { + os, exists := ms.Outputs[name] + if !exists { + // Not evaluated yet? + vals[name] = cty.DynamicVal + continue + } + + // The state structures haven't yet been updated to the new type system, + // so we'll need to shim here. + // FIXME: Remove this once ms.Outputs itself contains cty.Value. + vals[name] = hcl2shim.HCL2ValueFromConfigValue(os.Value) + } + return cty.ObjectVal(vals), diags } -func (d *evaluationStateData) GetModuleInstanceOutput(addrs.ModuleCallOutput, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) GetModuleInstanceOutput(addr addrs.ModuleCallOutput, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // Output results live in the module that declares them, which is one of + // the child module instances of our current module path. + absAddr := addr.AbsOutputValue(d.ModulePath) + moduleAddr := absAddr.Module + + // First we'll consult the configuration to see if an output of this + // name is declared at all. + moduleConfig := d.Evaluator.Config.DescendentForInstance(moduleAddr) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("output value read from %s, which has no configuration", moduleAddr)) + } + + config := moduleConfig.Module.Outputs[addr.Name] + if config == nil { + var suggestions []string + for k := range moduleConfig.Module.Outputs { + suggestions = append(suggestions, k) + } + suggestion := nameSuggestion(addr.Name, suggestions) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to undeclared output value`, + Detail: fmt.Sprintf(`An output value with the name %q has not been declared in %s.%s`, addr.Name, moduleAddr, suggestion), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + // Now we'll retrieve the value from the state, which means we need to hold + // the state lock. + d.Evaluator.StateLock.RLock() + defer d.Evaluator.StateLock.RUnlock() + + ms := d.Evaluator.State.ModuleByPath(moduleAddr) + if ms == nil { + // Not evaluated yet? + return cty.DynamicVal, diags + } + + os, exists := ms.Outputs[addr.Name] + if !exists { + // Not evaluated yet? + return cty.DynamicVal, diags + } + + // The state structures haven't yet been updated to the new type system, + // so we'll need to shim here. + // FIXME: Remove this once ms.Outputs itself contains cty.Value. + val := hcl2shim.HCL2ValueFromConfigValue(os.Value) + + return val, diags + } -func (d *evaluationStateData) GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + switch addr.Name { + + case "cwd": + wd, err := os.Getwd() + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Failed to get working directory`, + Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + return cty.StringVal(wd), diags + + case "module": + moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("module.path read from module %s, which has no configuration", d.ModulePath)) + } + sourceDir := moduleConfig.Module.SourceDir + return cty.StringVal(sourceDir), diags + + case "root": + sourceDir := d.Evaluator.Config.Module.SourceDir + return cty.StringVal(sourceDir), diags + + default: + suggestion := nameSuggestion(addr.Name, []string{"cwd", "module", "root"}) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "path" attribute`, + Detail: fmt.Sprintf(`The "path" object does not have an attribute named %q.%s`, addr.Name, suggestion), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } } -func (d *evaluationStateData) GetResourceInstance(addrs.ResourceInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) GetResourceInstance(addr addrs.ResourceInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // Although we are giving a ResourceInstance address here, if it has + // a key of addrs.NoKey then it might actually be a request for all of + // the instances of a particular resource. The reference resolver can't + // resolve the ambiguity itself, so we must do it in here. + + // We need to shim our address to the legacy form still used in the state structs. + addrKey := NewLegacyResourceInstanceAddress(addr.Absolute(d.ModulePath)).stateId() + + // We'll get the values for the instance(s) from state, so we'll need a read lock. + d.Evaluator.StateLock.RLock() + defer d.Evaluator.StateLock.RUnlock() + + ms := d.Evaluator.State.ModuleByPath(d.ModulePath) + if ms == nil { + // Not evaluated yet? + return cty.DynamicVal, diags + } + + // Note that the state structs currently have confusing legacy names: + // ResourceState is actually the state for what we call an "instance" + // elsewhere, and then InstanceState is the state for a particular _phase_ + // of that instance (primary vs. deposed). This should be addressed when + // we revise the state structs to natively support the HCL type system. + rs := ms.Resources[addrKey] + + // If we have an exact match for the requested instance and it has non-nil + // primary data then we'll use it directly. This is the easy path. + if rs != nil && rs.Primary != nil { + providerAddr, err := rs.ProviderAddr() + if err != nil { + // This indicates corruption of or tampering with the state file + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid provider address in state`, + Detail: fmt.Sprintf("The state for the referenced resource refers to a syntactically-invalid provider address %q. This can occur if the state data is incorrectly edited by hand.", rs.Provider), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + return d.getResourceInstanceSingle(addr, rng, rs.Primary, providerAddr) + } + + // If we get down here then we might have a request for the list of all + // instances of a particular resource, but only if we have a no-key address. + // If we have a _keyed_ address then instead it's a single instance that + // isn't evaluated yet. + if addr.Key != addrs.NoKey { + return d.getResourceInstancePending(addr, rng) + } + + return d.getResourceInstancesAll(addr.ContainingResource(), rng, ms) } -func (d *evaluationStateData) GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - panic("not yet implemented") +func (d *evaluationStateData) getResourceInstanceSingle(addr addrs.ResourceInstance, rng tfdiags.SourceRange, is *InstanceState, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // To properly decode the "flatmap"-based values from the state, we need + // to know the resource's schema, which we should already have cached + // from when the provider was initialized. + schema := d.getResourceSchema(addr.ContainingResource(), providerAddr) + if schema == nil { + // This shouldn't happen, since validation before we get here should've + // taken care of it, but we'll show a reasonable error message anyway. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Missing resource type schema`, + Detail: fmt.Sprintf("No schema is available for %s in %s. This is a bug in Terraform and should be reported.", addr, providerAddr), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + flatmapVal := is.Attributes + ty := schema.ImpliedType() + val, err := hcl2shim.HCL2ValueFromFlatmap(flatmapVal, ty) + if err != nil { + // A value in the flatmap value could not be conformed to the schema + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid value in state`, + Detail: fmt.Sprintf("The state data stored for %s does not conform to the resource schema: %s", addr, err), + Subject: rng.ToHCL().Ptr(), + }) + return cty.UnknownVal(ty), diags + } + + return val, diags +} + +func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng tfdiags.SourceRange, ms *ModuleState) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // Currently the only multi-instance construct we support is "count", which + // ensures that all of the instances will have integer keys, and so we + // can produce a tuple value of them. + // + // The legacy state structs are not designed to unambigiously represent + // a list of instances associated with a resource, and so we need to infer + // what exists based on which keys we find. Our returned tuple is therefore + // long enough to accommodate the highest index we find, and may contain + // unknown values filling in any "gaps" for instances that have been + // tainted or not yet created. + + // Keys in the resources map are resource addresses followed by a period + // and then an integer index. Keys without an integer index are possible + // too, but we already took care of those in GetResourceInstance by + // branching directly into getResourceInstanceSingle, so we know that + // we're dealing with keyed instances here. + prefix := addr.String() + "." + length := 0 + instanceVals := map[addrs.InstanceKey]cty.Value{} + for fullKey, rs := range ms.Resources { + if !strings.HasPrefix(fullKey, prefix) { + continue + } + if rs.Primary == nil { + continue + } + + keyStr := fullKey[len(prefix):] + var key addrs.InstanceKey + if i, err := strconv.Atoi(keyStr); err == nil { + key = addrs.IntKey(i) + if i >= length { + length = i + 1 + } + } else { + key = addrs.StringKey(keyStr) + } + + providerAddr, err := rs.ProviderAddr() + if err != nil { + // This indicates corruption of or tampering with the state file + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid provider address in state`, + Detail: fmt.Sprintf("The state for %s refers to a syntactically-invalid provider address %q. This can occur if the state data is incorrectly edited by hand.", addr.Instance(key), rs.Provider), + Subject: rng.ToHCL().Ptr(), + }) + continue + } + + val, instanceDiags := d.getResourceInstanceSingle(addr.Instance(key), rng, rs.Primary, providerAddr) + diags = diags.Append(instanceDiags) + + instanceVals[key] = val + } + + // TODO: In future, when for_each is implemented, we'll need to decide here + // whether to return a tuple value or an object value. However, by that + // time we should've revised the state structs so we can see unambigously + // which to use, rather than trying to guess based on the presence of + // keys. + + valsSeq := make([]cty.Value, length) + for i := 0; i < length; i++ { + val, exists := instanceVals[addrs.IntKey(i)] + if exists { + valsSeq[i] = val + } else { + // FIXME: Ideally we'd return an unknown value of the schema's + // implied type here, but this shim-ish implementation of resource + // evaluation is already tricky enough so we'll just cheat for + // now. Once we refactor for the new state format, reorganize this + // code so that the schema is available here. + valsSeq[i] = cty.DynamicVal // not yet known + } + } + + // We use a tuple rather than a list here because resource schemas may + // include dynamically-typed attributes, which will then cause each + // instance to potentially have a different runtime type. + return cty.TupleVal(valsSeq), diags +} + +func (d *evaluationStateData) getResourceInstancePending(addr addrs.ResourceInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // We'd ideally like to return a properly-typed unknown value here, in + // order to give the type checker maximum information to detect type + // mismatches even if concrete values aren't yet known. + // + // To do this we need to know the resource's schema, which we should + // already have cached from when the provider was initialized. However, we + // first need to look in configuration to find out which provider address + // will be responsible for creating this. + moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("reference to instance from %s, which has no configuration", d.ModulePath)) + } + + // Everything after here is best-effort: if we can't gather enough + // information to return a typed value then we'll give up and return an + // entirely-untyped value, assuming that we're in a special situation + // such as accessing an orphaned resource, which should get error-checked + // elsewhere. + rc := moduleConfig.Module.ResourceByAddr(addr.ContainingResource()) + if rc == nil { + return cty.DynamicVal, diags + } + providerAddr := rc.ProviderConfigAddr().Absolute(d.ModulePath) + schema := d.getResourceSchema(addr.ContainingResource(), providerAddr) + if schema == nil { + return cty.DynamicVal, diags + } + + return cty.UnknownVal(schema.ImpliedType()), diags +} + +func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.AbsProviderConfig) *configschema.Block { + d.Evaluator.ProvidersLock.Lock() + defer d.Evaluator.ProvidersLock.Unlock() + + providerSchema := d.Evaluator.ProviderSchemas[providerAddr.String()] + if providerSchema == nil { + return nil + } + + var schema *configschema.Block + switch addr.Mode { + case addrs.ManagedResourceMode: + schema = providerSchema.ResourceTypes[addr.Type] + case addrs.DataResourceMode: + schema = providerSchema.DataSources[addr.Type] + } + return schema +} + +func (d *evaluationStateData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + switch addr.Name { + + case "workspace": + workspaceName := d.Evaluator.Meta.Env + return cty.StringVal(workspaceName), diags + + case "env": + // Prior to Terraform 0.12 there was an attribute "env", which was + // an alias name for "workspace". This was deprecated and is now + // removed. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "terraform" attribute`, + Detail: `The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was rename to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`, + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "terraform" attribute`, + Detail: fmt.Sprintf(`The "terraform" object does not have an attribute named %q. The only supported attribute is terraform.workspace, the name of the currently-selected workspace.`, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } +} + +// nameSuggestion tries to find a name from the given slice of suggested names +// that is close to the given name and returns it if found. If no suggestion +// is close enough, returns the empty string. +// +// The suggestions are tried in order, so earlier suggestions take precedence +// if the given string is similar to two or more suggestions. +// +// This function is intended to be used with a relatively-small number of +// suggestions. It's not optimized for hundreds or thousands of them. +func nameSuggestion(given string, suggestions []string) string { + for _, suggestion := range suggestions { + dist := levenshtein.Distance(given, suggestion, nil) + if dist < 3 { // threshold determined experimentally + return suggestion + } + } + return "" } diff --git a/terraform/evaluate_test.go b/terraform/evaluate_test.go new file mode 100644 index 000000000..65675c3d5 --- /dev/null +++ b/terraform/evaluate_test.go @@ -0,0 +1,33 @@ +package terraform + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/tfdiags" +) + +func TestEvaluatorGetTerraformAttr(t *testing.T) { + evaluator := &Evaluator{ + Meta: &ContextMeta{ + Env: "foo", + }, + } + scope := evaluator.Scope(addrs.RootModuleInstance, nil) + + t.Run("workspace", func(t *testing.T) { + want := cty.StringVal("foo") + got, diags := scope.Data.GetTerraformAttr(addrs.TerraformAttr{ + Name: "workspace", + }, tfdiags.SourceRange{}) + if len(diags) != 0 { + t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) + } + if !got.RawEquals(want) { + t.Errorf("wrong result %q; want %q", got, want) + } + }) +}