opentofu/config/raw_config.go
Martin Atkins 39e609d5fd vendor: switch to HCL 2.0 in the HCL repository
Previously we were using the experimental HCL 2 repository, but now we'll
shift over to the v2 import path within the main HCL repository as part of
actually releasing HCL 2.0 as stable.

This is a mechanical search/replace to the new import paths. It also
switches to the v2.0.0 release of HCL, which includes some new code that
Terraform didn't previously have but should not change any behavior that
matters for Terraform's purposes.

For the moment the experimental HCL2 repository is still an indirect
dependency via terraform-config-inspect, so it remains in our go.sum and
vendor directories for the moment. Because terraform-config-inspect uses
a much smaller subset of the HCL2 functionality, this does still manage
to prune the vendor directory a little. A subsequent release of
terraform-config-inspect should allow us to completely remove that old
repository in a future commit.
2019-10-02 15:10:21 -07:00

451 lines
12 KiB
Go

package config
import (
"bytes"
"encoding/gob"
"errors"
"strconv"
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/mitchellh/copystructure"
"github.com/mitchellh/reflectwalk"
)
// RawConfig is a structure that holds a piece of configuration
// where the overall structure is unknown since it will be used
// to configure a plugin or some other similar external component.
//
// RawConfigs can be interpolated with variables that come from
// other resources, user variables, etc.
//
// RawConfig supports a query-like interface to request
// information from deep within the structure.
type RawConfig struct {
Key string
// Only _one_ of Raw and Body may be populated at a time.
//
// In the normal case, Raw is populated and Body is nil.
//
// When the experimental HCL2 parsing mode is enabled, "Body"
// is populated and RawConfig serves only to transport the hcl2.Body
// through the rest of Terraform core so we can ultimately decode it
// once its schema is known.
//
// Once we transition to HCL2 as the primary representation, RawConfig
// should be removed altogether and the hcl2.Body should be passed
// around directly.
Raw map[string]interface{}
Body hcl2.Body
Interpolations []ast.Node
Variables map[string]InterpolatedVariable
lock sync.Mutex
config map[string]interface{}
unknownKeys []string
}
// NewRawConfig creates a new RawConfig structure and populates the
// publicly readable struct fields.
func NewRawConfig(raw map[string]interface{}) (*RawConfig, error) {
result := &RawConfig{Raw: raw}
if err := result.init(); err != nil {
return nil, err
}
return result, nil
}
// NewRawConfigHCL2 creates a new RawConfig that is serving as a capsule
// to transport a hcl2.Body. In this mode, the publicly-readable struct
// fields are not populated since all operations should instead be diverted
// to the HCL2 body.
//
// For a RawConfig object constructed with this function, the only valid use
// is to later retrieve the Body value and call its own methods. Callers
// may choose to set and then later handle the Key field, in a manner
// consistent with how it is handled by the Value method, but the Value
// method itself must not be used.
//
// This is an experimental codepath to be used only by the HCL2 config loader.
// Non-experimental parsing should _always_ use NewRawConfig to produce a
// fully-functional RawConfig object.
func NewRawConfigHCL2(body hcl2.Body) *RawConfig {
return &RawConfig{
Body: body,
}
}
// RawMap returns a copy of the RawConfig.Raw map.
func (r *RawConfig) RawMap() map[string]interface{} {
r.lock.Lock()
defer r.lock.Unlock()
m := make(map[string]interface{})
for k, v := range r.Raw {
m[k] = v
}
return m
}
// Copy returns a copy of this RawConfig, uninterpolated.
func (r *RawConfig) Copy() *RawConfig {
if r == nil {
return nil
}
r.lock.Lock()
defer r.lock.Unlock()
if r.Body != nil {
return NewRawConfigHCL2(r.Body)
}
newRaw := make(map[string]interface{})
for k, v := range r.Raw {
newRaw[k] = v
}
result, err := NewRawConfig(newRaw)
if err != nil {
panic("copy failed: " + err.Error())
}
result.Key = r.Key
return result
}
// Value returns the value of the configuration if this configuration
// has a Key set. If this does not have a Key set, nil will be returned.
func (r *RawConfig) Value() interface{} {
if c := r.Config(); c != nil {
if v, ok := c[r.Key]; ok {
return v
}
}
r.lock.Lock()
defer r.lock.Unlock()
return r.Raw[r.Key]
}
// Config returns the entire configuration with the variables
// interpolated from any call to Interpolate.
//
// If any interpolated variables are unknown (value set to
// UnknownVariableValue), the first non-container (map, slice, etc.) element
// will be removed from the config. The keys of unknown variables
// can be found using the UnknownKeys function.
//
// By pruning out unknown keys from the configuration, the raw
// structure will always successfully decode into its ultimate
// structure using something like mapstructure.
func (r *RawConfig) Config() map[string]interface{} {
r.lock.Lock()
defer r.lock.Unlock()
return r.config
}
// Interpolate uses the given mapping of variable values and uses
// those as the values to replace any variables in this raw
// configuration.
//
// Any prior calls to Interpolate are replaced with this one.
//
// If a variable key is missing, this will panic.
func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
r.lock.Lock()
defer r.lock.Unlock()
config := langEvalConfig(vs)
return r.interpolate(func(root ast.Node) (interface{}, error) {
// None of the variables we need are computed, meaning we should
// be able to properly evaluate.
result, err := hil.Eval(root, config)
if err != nil {
return "", err
}
return result.Value, nil
})
}
// Merge merges another RawConfig into this one (overriding any conflicting
// values in this config) and returns a new config. The original config
// is not modified.
func (r *RawConfig) Merge(other *RawConfig) *RawConfig {
r.lock.Lock()
defer r.lock.Unlock()
// Merge the raw configurations
raw := make(map[string]interface{})
for k, v := range r.Raw {
raw[k] = v
}
for k, v := range other.Raw {
raw[k] = v
}
// Create the result
result, err := NewRawConfig(raw)
if err != nil {
panic(err)
}
// Merge the interpolated results
result.config = make(map[string]interface{})
for k, v := range r.config {
result.config[k] = v
}
for k, v := range other.config {
result.config[k] = v
}
// Build the unknown keys
if len(r.unknownKeys) > 0 || len(other.unknownKeys) > 0 {
unknownKeys := make(map[string]struct{})
for _, k := range r.unknownKeys {
unknownKeys[k] = struct{}{}
}
for _, k := range other.unknownKeys {
unknownKeys[k] = struct{}{}
}
result.unknownKeys = make([]string, 0, len(unknownKeys))
for k, _ := range unknownKeys {
result.unknownKeys = append(result.unknownKeys, k)
}
}
return result
}
func (r *RawConfig) init() error {
r.lock.Lock()
defer r.lock.Unlock()
r.config = r.Raw
r.Interpolations = nil
r.Variables = nil
fn := func(node ast.Node) (interface{}, error) {
r.Interpolations = append(r.Interpolations, node)
vars, err := DetectVariables(node)
if err != nil {
return "", err
}
for _, v := range vars {
if r.Variables == nil {
r.Variables = make(map[string]InterpolatedVariable)
}
r.Variables[v.FullKey()] = v
}
return "", nil
}
walker := &interpolationWalker{F: fn}
if err := reflectwalk.Walk(r.Raw, walker); err != nil {
return err
}
return nil
}
func (r *RawConfig) interpolate(fn interpolationWalkerFunc) error {
if r.Body != nil {
// For RawConfigs created for the HCL2 experiement, callers must
// use the HCL2 Body API directly rather than interpolating via
// the RawConfig.
return errors.New("this feature is not yet supported under the HCL2 experiment")
}
config, err := copystructure.Copy(r.Raw)
if err != nil {
return err
}
r.config = config.(map[string]interface{})
w := &interpolationWalker{F: fn, Replace: true}
err = reflectwalk.Walk(r.config, w)
if err != nil {
return err
}
r.unknownKeys = w.unknownKeys
return nil
}
func (r *RawConfig) merge(r2 *RawConfig) *RawConfig {
if r == nil && r2 == nil {
return nil
}
if r == nil {
r = &RawConfig{}
}
rawRaw, err := copystructure.Copy(r.Raw)
if err != nil {
panic(err)
}
raw := rawRaw.(map[string]interface{})
if r2 != nil {
for k, v := range r2.Raw {
raw[k] = v
}
}
result, err := NewRawConfig(raw)
if err != nil {
panic(err)
}
return result
}
// couldBeInteger is a helper that determines if the represented value could
// result in an integer.
//
// This function only works for RawConfigs that have "Key" set, meaning that
// a single result can be produced. Calling this function will overwrite
// the Config and Value results to be a test value.
//
// This function is conservative. If there is some doubt about whether the
// result could be an integer -- for example, if it depends on a variable
// whose type we don't know yet -- it will still return true.
func (r *RawConfig) couldBeInteger() bool {
if r.Key == "" {
// un-keyed RawConfigs can never produce numbers
return false
}
if r.Body == nil {
// Normal path: using the interpolator in this package
// Interpolate with a fixed number to verify that its a number.
r.interpolate(func(root ast.Node) (interface{}, error) {
// Execute the node but transform the AST so that it returns
// a fixed value of "5" for all interpolations.
result, err := hil.Eval(
hil.FixedValueTransform(
root, &ast.LiteralNode{Value: "5", Typex: ast.TypeString}),
nil)
if err != nil {
return "", err
}
return result.Value, nil
})
_, err := strconv.ParseInt(r.Value().(string), 0, 0)
return err == nil
} else {
// HCL2 experiment path: using the HCL2 API via shims
//
// This path catches fewer situations because we have to assume all
// variables are entirely unknown in HCL2, rather than the assumption
// above that all variables can be numbers because names like "var.foo"
// are considered a single variable rather than an attribute access.
// This is fine in practice, because we get a definitive answer
// during the graph walk when we have real values to work with.
attrs, diags := r.Body.JustAttributes()
if diags.HasErrors() {
// This body is not just a single attribute with a value, so
// this can't be a number.
return false
}
attr, hasAttr := attrs[r.Key]
if !hasAttr {
return false
}
result, diags := hcl2EvalWithUnknownVars(attr.Expr)
if diags.HasErrors() {
// We'll conservatively assume that this error is a result of
// us not being ready to fully-populate the scope, and catch
// any further problems during the main graph walk.
return true
}
// If the result is convertable to number then we'll allow it.
// We do this because an unknown string is optimistically convertable
// to number (might be "5") but a _known_ string "hello" is not.
_, err := convert.Convert(result, cty.Number)
return err == nil
}
}
// UnknownKeys returns the keys of the configuration that are unknown
// because they had interpolated variables that must be computed.
func (r *RawConfig) UnknownKeys() []string {
r.lock.Lock()
defer r.lock.Unlock()
return r.unknownKeys
}
// See GobEncode
func (r *RawConfig) GobDecode(b []byte) error {
var data gobRawConfig
err := gob.NewDecoder(bytes.NewReader(b)).Decode(&data)
if err != nil {
return err
}
r.Key = data.Key
r.Raw = data.Raw
return r.init()
}
// GobEncode is a custom Gob encoder to use so that we only include the
// raw configuration. Interpolated variables and such are lost and the
// tree of interpolated variables is recomputed on decode, since it is
// referentially transparent.
func (r *RawConfig) GobEncode() ([]byte, error) {
r.lock.Lock()
defer r.lock.Unlock()
data := gobRawConfig{
Key: r.Key,
Raw: r.Raw,
}
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
type gobRawConfig struct {
Key string
Raw map[string]interface{}
}
// langEvalConfig returns the evaluation configuration we use to execute.
func langEvalConfig(vs map[string]ast.Variable) *hil.EvalConfig {
funcMap := make(map[string]ast.Function)
for k, v := range Funcs() {
funcMap[k] = v
}
funcMap["lookup"] = interpolationFuncLookup(vs)
funcMap["keys"] = interpolationFuncKeys(vs)
funcMap["values"] = interpolationFuncValues(vs)
return &hil.EvalConfig{
GlobalScope: &ast.BasicScope{
VarMap: vs,
FuncMap: funcMap,
},
}
}