mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
22fb82963c
Previously we were demoting HCL2 RawConfigs into empty old-school RawConfigs on copy.
457 lines
12 KiB
Go
457 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/hcl2/hcl"
|
|
"github.com/hashicorp/hil"
|
|
"github.com/hashicorp/hil/ast"
|
|
"github.com/mitchellh/copystructure"
|
|
"github.com/mitchellh/reflectwalk"
|
|
)
|
|
|
|
// UnknownVariableValue is a sentinel value that can be used
|
|
// to denote that the value of a variable is unknown at this time.
|
|
// RawConfig uses this information to build up data about
|
|
// unknown keys.
|
|
const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
|
|
|
|
// 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,
|
|
},
|
|
}
|
|
}
|