terraformutils/hcl.go (267 lines of code) (raw):

// Copyright 2018 The Terraformer Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package terraformutils import ( "bytes" "encoding/json" "errors" "fmt" "log" "regexp" "sort" "strings" "github.com/hashicorp/hcl/hcl/ast" hclPrinter "github.com/hashicorp/hcl/hcl/printer" hclParser "github.com/hashicorp/hcl/json/parser" ) // Copy code from https://github.com/kubernetes/kops project with few changes for support many provider and heredoc const safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" var unsafeChars = regexp.MustCompile(`[^0-9A-Za-z_\-]`) // make HCL output reproducible by sorting the AST nodes func sortHclTree(tree interface{}) { switch t := tree.(type) { case []*ast.ObjectItem: sort.Slice(t, func(i, j int) bool { var bI, bJ bytes.Buffer _, _ = hclPrinter.Fprint(&bI, t[i]), hclPrinter.Fprint(&bJ, t[j]) return bI.String() < bJ.String() }) case []ast.Node: sort.Slice(t, func(i, j int) bool { var bI, bJ bytes.Buffer _, _ = hclPrinter.Fprint(&bI, t[i]), hclPrinter.Fprint(&bJ, t[j]) return bI.String() < bJ.String() }) default: } } // sanitizer fixes up an invalid HCL AST, as produced by the HCL parser for JSON type astSanitizer struct { sort bool } // output prints creates b printable HCL output and returns it. func (v *astSanitizer) visit(n interface{}) { switch t := n.(type) { case *ast.File: v.visit(t.Node) case *ast.ObjectList: var index int if v.sort { sortHclTree(t.Items) } for { if index == len(t.Items) { break } v.visit(t.Items[index]) index++ } case *ast.ObjectKey: case *ast.ObjectItem: v.visitObjectItem(t) case *ast.LiteralType: case *ast.ListType: if v.sort { sortHclTree(t.List) } case *ast.ObjectType: if v.sort { sortHclTree(t.List) } v.visit(t.List) default: fmt.Printf(" unknown type: %T\n", n) } } func (v *astSanitizer) visitObjectItem(o *ast.ObjectItem) { for i, k := range o.Keys { if i == 0 { text := k.Token.Text if text != "" && text[0] == '"' && text[len(text)-1] == '"' { v := text[1 : len(text)-1] safe := true for _, c := range v { if !strings.ContainsRune(safeChars, c) { safe = false break } } if strings.HasPrefix(v, "--") { // if the key starts with "--", we must quote it. Seen in aws_glue_job.default_arguments parameter v = fmt.Sprintf(`"%s"`, v) } if safe { k.Token.Text = v } } } } switch t := o.Val.(type) { case *ast.LiteralType: // heredoc support if strings.HasPrefix(t.Token.Text, `"<<`) { t.Token.Text = t.Token.Text[1:] t.Token.Text = t.Token.Text[:len(t.Token.Text)-1] t.Token.Text = strings.ReplaceAll(t.Token.Text, `\n`, "\n") t.Token.Text = strings.ReplaceAll(t.Token.Text, `\t`, "") t.Token.Type = 10 // check if text json for Unquote and Indent jsonTest := t.Token.Text lines := strings.Split(jsonTest, "\n") jsonTest = strings.Join(lines[1:len(lines)-1], "\n") jsonTest = strings.ReplaceAll(jsonTest, "\\\"", "\"") // it's json we convert to heredoc back var tmp interface{} = map[string]interface{}{} err := json.Unmarshal([]byte(jsonTest), &tmp) if err != nil { tmp = make([]interface{}, 0) err = json.Unmarshal([]byte(jsonTest), &tmp) } if err == nil { dataJSONBytes, err := json.MarshalIndent(tmp, "", " ") if err == nil { jsonData := strings.Split(string(dataJSONBytes), "\n") // first line for heredoc jsonData = append([]string{lines[0]}, jsonData...) // last line for heredoc jsonData = append(jsonData, lines[len(lines)-1]) hereDoc := strings.Join(jsonData, "\n") t.Token.Text = hereDoc } } } case *ast.ListType: sortHclTree(t.List) default: } // A hack so that Assign.IsValid is true, so that the printer will output = o.Assign.Line = 1 v.visit(o.Val) } func Print(data interface{}, mapsObjects map[string]struct{}, format string, sort bool) ([]byte, error) { switch format { case "hcl": return hclPrint(data, mapsObjects, sort) case "json": return jsonPrint(data) } return []byte{}, errors.New("error: unknown output format") } func hclPrint(data interface{}, mapsObjects map[string]struct{}, sort bool) ([]byte, error) { dataBytesJSON, err := jsonPrint(data) if err != nil { return dataBytesJSON, err } dataJSON := string(dataBytesJSON) nodes, err := hclParser.Parse([]byte(dataJSON)) if err != nil { log.Println(dataJSON) return []byte{}, fmt.Errorf("error parsing terraform json: %v", err) } var sanitizer astSanitizer sanitizer.sort = sort sanitizer.visit(nodes) var b bytes.Buffer err = hclPrinter.Fprint(&b, nodes) if err != nil { return nil, fmt.Errorf("error writing HCL: %v", err) } s := b.String() // Remove extra whitespace... s = strings.ReplaceAll(s, "\n\n", "\n") // ...but leave whitespace between resources s = strings.ReplaceAll(s, "}\nresource", "}\n\nresource") // Apply Terraform style (alignment etc.) formatted, err := hclPrinter.Format([]byte(s)) if err != nil { return nil, err } // hack for support terraform 0.12 formatted = terraform12Adjustments(formatted, mapsObjects) // hack for support terraform 0.13 formatted = terraform13Adjustments(formatted) if err != nil { log.Println("Invalid HCL follows:") for i, line := range strings.Split(s, "\n") { fmt.Printf("%4d|\t%s\n", i+1, line) } return nil, fmt.Errorf("error formatting HCL: %v", err) } return formatted, nil } func terraform12Adjustments(formatted []byte, mapsObjects map[string]struct{}) []byte { singletonListFix := regexp.MustCompile(`^\s*\w+ = {`) singletonListFixEnd := regexp.MustCompile(`^\s*}`) s := string(formatted) old := " = {" newEquals := " {" lines := strings.Split(s, "\n") prefix := make([]string, 0) for i, line := range lines { if singletonListFixEnd.MatchString(line) && len(prefix) > 0 { prefix = prefix[:len(prefix)-1] continue } if !singletonListFix.MatchString(line) { continue } key := strings.Trim(strings.Split(line, old)[0], " ") prefix = append(prefix, key) if _, exist := mapsObjects[strings.Join(prefix, ".")]; exist { continue } lines[i] = strings.ReplaceAll(line, old, newEquals) } s = strings.Join(lines, "\n") return []byte(s) } func terraform13Adjustments(formatted []byte) []byte { s := string(formatted) requiredProvidersRe := regexp.MustCompile("required_providers \".*\" {") endBraceRe := regexp.MustCompile(`^\s*}`) lines := strings.Split(s, "\n") for i, line := range lines { if requiredProvidersRe.MatchString(line) { parts := strings.Split(strings.TrimSpace(line), " ") provider := strings.ReplaceAll(parts[1], "\"", "") lines[i] = "\trequired_providers {" var innerBlock []string inner := i + 1 for ; !endBraceRe.MatchString(lines[inner]); inner++ { innerBlock = append(innerBlock, "\t"+lines[inner]) } lines[i+1] = "\t\t" + provider + " = {\n" + strings.Join(innerBlock, "\n") + "\n\t\t}" lines = append(lines[:i+2], lines[inner:]...) break } } s = strings.Join(lines, "\n") return []byte(s) } func escapeRune(s string) string { return fmt.Sprintf("-%04X-", s) } // Sanitize name for terraform style func TfSanitize(name string) string { name = unsafeChars.ReplaceAllStringFunc(name, escapeRune) name = "tfer--" + name return name } // Print hcl file from TerraformResource + provider func HclPrintResource(resources []Resource, providerData map[string]interface{}, output string, sort bool) ([]byte, error) { resourcesByType := map[string]map[string]interface{}{} mapsObjects := map[string]struct{}{} indexRe := regexp.MustCompile(`\.[0-9]+`) for _, res := range resources { r := resourcesByType[res.InstanceInfo.Type] if r == nil { r = make(map[string]interface{}) resourcesByType[res.InstanceInfo.Type] = r } if r[res.ResourceName] != nil { log.Println(resources) log.Printf("[ERR]: duplicate resource found: %s.%s", res.InstanceInfo.Type, res.ResourceName) continue } r[res.ResourceName] = res.Item for k := range res.InstanceState.Attributes { if strings.HasSuffix(k, ".%") { key := strings.TrimSuffix(k, ".%") mapsObjects[indexRe.ReplaceAllString(key, "")] = struct{}{} } } } data := map[string]interface{}{} if len(resourcesByType) > 0 { data["resource"] = resourcesByType } if len(providerData) > 0 { data["provider"] = providerData } var err error hclBytes, err := Print(data, mapsObjects, output, sort) if err != nil { return []byte{}, err } return hclBytes, nil }