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 }