terraform/internal/command/jsonconfig/expression.go

178 lines
6.1 KiB
Go
Raw Normal View History

package jsonconfig
import (
"bytes"
"encoding/json"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
// expression represents any unparsed expression
type expression struct {
// "constant_value" is set only if the expression contains no references to
// other objects, in which case it gives the resulting constant value. This
// is mapped as for the individual values in the common value
// representation.
ConstantValue json.RawMessage `json:"constant_value,omitempty"`
// Alternatively, "references" will be set to a list of references in the
// expression. Multi-step references will be unwrapped and duplicated for
// each significant traversal step, allowing callers to more easily
// recognize the objects they care about without attempting to parse the
// expressions. Callers should only use string equality checks here, since
// the syntax may be extended in future releases.
References []string `json:"references,omitempty"`
}
func marshalExpression(ex hcl.Expression) expression {
var ret expression
if ex == nil {
return ret
}
val, _ := ex.Value(nil)
if val != cty.NilVal {
valJSON, _ := ctyjson.Marshal(val, val.Type())
ret.ConstantValue = valJSON
}
refs, _ := lang.ReferencesInExpr(ex)
if len(refs) > 0 {
var varString []string
for _, ref := range refs {
// We work backwards here, starting with the full reference +
// reamining traversal, and then unwrapping the remaining traversals
// into parts until we end up at the smallest referencable address.
remains := ref.Remaining
for len(remains) > 0 {
varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, traversalStr(remains)))
remains = remains[:(len(remains) - 1)]
}
varString = append(varString, ref.Subject.String())
switch ref.Subject.(type) {
case addrs.ModuleCallInstance:
if ref.Subject.(addrs.ModuleCallInstance).Key != addrs.NoKey {
// Include the module call, without the key
varString = append(varString, ref.Subject.(addrs.ModuleCallInstance).Call.String())
}
case addrs.ResourceInstance:
if ref.Subject.(addrs.ResourceInstance).Key != addrs.NoKey {
// Include the resource, without the key
varString = append(varString, ref.Subject.(addrs.ResourceInstance).Resource.String())
}
case addrs.ModuleCallInstanceOutput:
// Include the module name, without the output name
varString = append(varString, ref.Subject.(addrs.ModuleCallInstanceOutput).Call.String())
}
}
ret.References = varString
}
return ret
}
func (e *expression) Empty() bool {
return e.ConstantValue == nil && e.References == nil
}
// expressions is used to represent the entire content of a block. Attribute
// arguments are mapped directly with the attribute name as key and an
// expression as value.
type expressions map[string]interface{}
func marshalExpressions(body hcl.Body, schema *configschema.Block) expressions {
// Since we want the raw, un-evaluated expressions we need to use the
// low-level HCL API here, rather than the hcldec decoder API. That means we
// need the low-level schema.
lowSchema := hcldec.ImpliedSchema(schema.DecoderSpec())
// (lowSchema is an hcl.BodySchema:
// https://godoc.org/github.com/hashicorp/hcl/v2/hcl#BodySchema )
// Use the low-level schema with the body to decode one level We'll just
// ignore any additional content that's not covered by the schema, which
// will effectively ignore "dynamic" blocks, and may also ignore other
// unknown stuff but anything else would get flagged by Terraform as an
// error anyway, and so we wouldn't end up in here.
content, _, _ := body.PartialContent(lowSchema)
if content == nil {
// Should never happen for a valid body, but we'll just generate empty
// if there were any problems.
return nil
}
ret := make(expressions)
// Any attributes we encode directly as expression objects.
for name, attr := range content.Attributes {
ret[name] = marshalExpression(attr.Expr) // note: singular expression for this one
}
// Any nested blocks require a recursive call to produce nested expressions
// objects.
for _, block := range content.Blocks {
typeName := block.Type
blockS, exists := schema.BlockTypes[typeName]
if !exists {
// Should never happen since only block types in the schema would be
// put in blocks list
continue
}
switch blockS.Nesting {
configs/configschema: Introduce the NestingGroup mode for blocks In study of existing providers we've found a pattern we werent previously accounting for of using a nested block type to represent a group of arguments that relate to a particular feature that is always enabled but where it improves configuration readability to group all of its settings together in a nested block. The existing NestingSingle was not a good fit for this because it is designed under the assumption that the presence or absence of the block has some significance in enabling or disabling the relevant feature, and so for these always-active cases we'd generate a misleading plan where the settings for the feature appear totally absent, rather than showing the default values that will be selected. NestingGroup is, therefore, a slight variation of NestingSingle where presence vs. absence of the block is not distinguishable (it's never null) and instead its contents are treated as unset when the block is absent. This then in turn causes any default values associated with the nested arguments to be honored and displayed in the plan whenever the block is not explicitly configured. The current SDK cannot activate this mode, but that's okay because its "legacy type system" opt-out flag allows it to force a block to be processed in this way anyway. We're adding this now so that we can introduce the feature in a future SDK without causing a breaking change to the protocol, since the set of possible block nesting modes is not extensible.
2019-04-09 00:32:53 +02:00
case configschema.NestingSingle, configschema.NestingGroup:
ret[typeName] = marshalExpressions(block.Body, &blockS.Block)
case configschema.NestingList, configschema.NestingSet:
if _, exists := ret[typeName]; !exists {
ret[typeName] = make([]map[string]interface{}, 0, 1)
}
ret[typeName] = append(ret[typeName].([]map[string]interface{}), marshalExpressions(block.Body, &blockS.Block))
case configschema.NestingMap:
if _, exists := ret[typeName]; !exists {
ret[typeName] = make(map[string]map[string]interface{})
}
// NestingMap blocks always have the key in the first (and only) label
key := block.Labels[0]
retMap := ret[typeName].(map[string]map[string]interface{})
retMap[key] = marshalExpressions(block.Body, &blockS.Block)
}
}
return ret
}
// traversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
//
// This was copied (and simplified) from internal/command/views/json/diagnostic.go.
func traversalStr(traversal hcl.Traversal) string {
var buf bytes.Buffer
for _, step := range traversal {
switch tStep := step.(type) {
case hcl.TraverseRoot:
buf.WriteString(tStep.Name)
case hcl.TraverseAttr:
buf.WriteByte('.')
buf.WriteString(tStep.Name)
case hcl.TraverseIndex:
buf.WriteByte('[')
switch tStep.Key.Type() {
case cty.String:
buf.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
case cty.Number:
bf := tStep.Key.AsBigFloat()
buf.WriteString(bf.Text('g', 10))
}
buf.WriteByte(']')
}
}
return buf.String()
}