github.com/opentofu/opentofu@v1.7.1/internal/command/jsonconfig/expression.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package jsonconfig
     7  
     8  import (
     9  	"bytes"
    10  	"encoding/json"
    11  	"fmt"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hcldec"
    15  	"github.com/zclconf/go-cty/cty"
    16  	ctyjson "github.com/zclconf/go-cty/cty/json"
    17  
    18  	"github.com/opentofu/opentofu/internal/addrs"
    19  	"github.com/opentofu/opentofu/internal/configs/configschema"
    20  	"github.com/opentofu/opentofu/internal/lang"
    21  	"github.com/opentofu/opentofu/internal/lang/blocktoattr"
    22  )
    23  
    24  // expression represents any unparsed expression
    25  type expression struct {
    26  	// "constant_value" is set only if the expression contains no references to
    27  	// other objects, in which case it gives the resulting constant value. This
    28  	// is mapped as for the individual values in the common value
    29  	// representation.
    30  	ConstantValue json.RawMessage `json:"constant_value,omitempty"`
    31  
    32  	// Alternatively, "references" will be set to a list of references in the
    33  	// expression. Multi-step references will be unwrapped and duplicated for
    34  	// each significant traversal step, allowing callers to more easily
    35  	// recognize the objects they care about without attempting to parse the
    36  	// expressions. Callers should only use string equality checks here, since
    37  	// the syntax may be extended in future releases.
    38  	References []string `json:"references,omitempty"`
    39  }
    40  
    41  func marshalExpression(ex hcl.Expression) expression {
    42  	var ret expression
    43  	if ex == nil {
    44  		return ret
    45  	}
    46  
    47  	val, _ := ex.Value(nil)
    48  	if val != cty.NilVal {
    49  		valJSON, _ := ctyjson.Marshal(val, val.Type())
    50  		ret.ConstantValue = valJSON
    51  	}
    52  
    53  	refs, _ := lang.ReferencesInExpr(addrs.ParseRef, ex)
    54  	if len(refs) > 0 {
    55  		var varString []string
    56  		for _, ref := range refs {
    57  			// We work backwards here, starting with the full reference +
    58  			// reamining traversal, and then unwrapping the remaining traversals
    59  			// into parts until we end up at the smallest referencable address.
    60  			remains := ref.Remaining
    61  			for len(remains) > 0 {
    62  				varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, traversalStr(remains)))
    63  				remains = remains[:(len(remains) - 1)]
    64  			}
    65  			varString = append(varString, ref.Subject.String())
    66  
    67  			switch ref.Subject.(type) {
    68  			case addrs.ModuleCallInstance:
    69  				if ref.Subject.(addrs.ModuleCallInstance).Key != addrs.NoKey {
    70  					// Include the module call, without the key
    71  					varString = append(varString, ref.Subject.(addrs.ModuleCallInstance).Call.String())
    72  				}
    73  			case addrs.ResourceInstance:
    74  				if ref.Subject.(addrs.ResourceInstance).Key != addrs.NoKey {
    75  					// Include the resource, without the key
    76  					varString = append(varString, ref.Subject.(addrs.ResourceInstance).Resource.String())
    77  				}
    78  			case addrs.ModuleCallInstanceOutput:
    79  				// Include the module name, without the output name
    80  				varString = append(varString, ref.Subject.(addrs.ModuleCallInstanceOutput).Call.String())
    81  			}
    82  		}
    83  		ret.References = varString
    84  	}
    85  
    86  	return ret
    87  }
    88  
    89  func (e *expression) Empty() bool {
    90  	return e.ConstantValue == nil && e.References == nil
    91  }
    92  
    93  // expressions is used to represent the entire content of a block. Attribute
    94  // arguments are mapped directly with the attribute name as key and an
    95  // expression as value.
    96  type expressions map[string]interface{}
    97  
    98  func marshalExpressions(body hcl.Body, schema *configschema.Block) expressions {
    99  	// Since we want the raw, un-evaluated expressions we need to use the
   100  	// low-level HCL API here, rather than the hcldec decoder API. That means we
   101  	// need the low-level schema.
   102  	lowSchema := hcldec.ImpliedSchema(schema.DecoderSpec())
   103  	// (lowSchema is an hcl.BodySchema:
   104  	// https://godoc.org/github.com/hashicorp/hcl/v2/hcl#BodySchema )
   105  
   106  	// fix any ConfigModeAttr blocks present from legacy providers
   107  	body = blocktoattr.FixUpBlockAttrs(body, schema)
   108  
   109  	// Use the low-level schema with the body to decode one level We'll just
   110  	// ignore any additional content that's not covered by the schema, which
   111  	// will effectively ignore "dynamic" blocks, and may also ignore other
   112  	// unknown stuff but anything else would get flagged by OpenTofu as an
   113  	// error anyway, and so we wouldn't end up in here.
   114  	content, _, _ := body.PartialContent(lowSchema)
   115  	if content == nil {
   116  		// Should never happen for a valid body, but we'll just generate empty
   117  		// if there were any problems.
   118  		return nil
   119  	}
   120  
   121  	ret := make(expressions)
   122  
   123  	// Any attributes we encode directly as expression objects.
   124  	for name, attr := range content.Attributes {
   125  		ret[name] = marshalExpression(attr.Expr) // note: singular expression for this one
   126  	}
   127  
   128  	// Any nested blocks require a recursive call to produce nested expressions
   129  	// objects.
   130  	for _, block := range content.Blocks {
   131  		typeName := block.Type
   132  		blockS, exists := schema.BlockTypes[typeName]
   133  		if !exists {
   134  			// Should never happen since only block types in the schema would be
   135  			// put in blocks list
   136  			continue
   137  		}
   138  
   139  		switch blockS.Nesting {
   140  		case configschema.NestingSingle, configschema.NestingGroup:
   141  			ret[typeName] = marshalExpressions(block.Body, &blockS.Block)
   142  		case configschema.NestingList, configschema.NestingSet:
   143  			if _, exists := ret[typeName]; !exists {
   144  				ret[typeName] = make([]map[string]interface{}, 0, 1)
   145  			}
   146  			ret[typeName] = append(ret[typeName].([]map[string]interface{}), marshalExpressions(block.Body, &blockS.Block))
   147  		case configschema.NestingMap:
   148  			if _, exists := ret[typeName]; !exists {
   149  				ret[typeName] = make(map[string]map[string]interface{})
   150  			}
   151  			// NestingMap blocks always have the key in the first (and only) label
   152  			key := block.Labels[0]
   153  			retMap := ret[typeName].(map[string]map[string]interface{})
   154  			retMap[key] = marshalExpressions(block.Body, &blockS.Block)
   155  		}
   156  	}
   157  
   158  	return ret
   159  }
   160  
   161  // traversalStr produces a representation of an HCL traversal that is compact,
   162  // resembles HCL native syntax, and is suitable for display in the UI.
   163  //
   164  // This was copied (and simplified) from internal/command/views/json/diagnostic.go.
   165  func traversalStr(traversal hcl.Traversal) string {
   166  	var buf bytes.Buffer
   167  	for _, step := range traversal {
   168  		switch tStep := step.(type) {
   169  		case hcl.TraverseRoot:
   170  			buf.WriteString(tStep.Name)
   171  		case hcl.TraverseAttr:
   172  			buf.WriteByte('.')
   173  			buf.WriteString(tStep.Name)
   174  		case hcl.TraverseIndex:
   175  			buf.WriteByte('[')
   176  			switch tStep.Key.Type() {
   177  			case cty.String:
   178  				buf.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
   179  			case cty.Number:
   180  				bf := tStep.Key.AsBigFloat()
   181  				buf.WriteString(bf.Text('g', 10))
   182  			}
   183  			buf.WriteByte(']')
   184  		}
   185  	}
   186  	return buf.String()
   187  }