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