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 { 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() }