github.com/GoogleCloudPlatform/terraformer@v0.8.18/terraformutils/hcl.go (about) 1 // Copyright 2018 The Terraformer Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package terraformutils 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "log" 23 "regexp" 24 "strings" 25 26 "github.com/hashicorp/hcl/hcl/ast" 27 hclPrinter "github.com/hashicorp/hcl/hcl/printer" 28 hclParser "github.com/hashicorp/hcl/json/parser" 29 ) 30 31 // Copy code from https://github.com/kubernetes/kops project with few changes for support many provider and heredoc 32 33 const safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" 34 35 var unsafeChars = regexp.MustCompile(`[^0-9A-Za-z_\-]`) 36 37 // sanitizer fixes up an invalid HCL AST, as produced by the HCL parser for JSON 38 type astSanitizer struct{} 39 40 // output prints creates b printable HCL output and returns it. 41 func (v *astSanitizer) visit(n interface{}) { 42 switch t := n.(type) { 43 case *ast.File: 44 v.visit(t.Node) 45 case *ast.ObjectList: 46 var index int 47 for { 48 if index == len(t.Items) { 49 break 50 } 51 v.visit(t.Items[index]) 52 index++ 53 } 54 case *ast.ObjectKey: 55 case *ast.ObjectItem: 56 v.visitObjectItem(t) 57 case *ast.LiteralType: 58 case *ast.ListType: 59 case *ast.ObjectType: 60 v.visit(t.List) 61 default: 62 fmt.Printf(" unknown type: %T\n", n) 63 } 64 } 65 66 func (v *astSanitizer) visitObjectItem(o *ast.ObjectItem) { 67 for i, k := range o.Keys { 68 if i == 0 { 69 text := k.Token.Text 70 if text != "" && text[0] == '"' && text[len(text)-1] == '"' { 71 v := text[1 : len(text)-1] 72 safe := true 73 for _, c := range v { 74 if !strings.ContainsRune(safeChars, c) { 75 safe = false 76 break 77 } 78 } 79 if safe { 80 k.Token.Text = v 81 } 82 } 83 } 84 } 85 switch t := o.Val.(type) { 86 case *ast.LiteralType: // heredoc support 87 if strings.HasPrefix(t.Token.Text, `"<<`) { 88 t.Token.Text = t.Token.Text[1:] 89 t.Token.Text = t.Token.Text[:len(t.Token.Text)-1] 90 t.Token.Text = strings.ReplaceAll(t.Token.Text, `\n`, "\n") 91 t.Token.Text = strings.ReplaceAll(t.Token.Text, `\t`, "") 92 t.Token.Type = 10 93 // check if text json for Unquote and Indent 94 jsonTest := t.Token.Text 95 lines := strings.Split(jsonTest, "\n") 96 jsonTest = strings.Join(lines[1:len(lines)-1], "\n") 97 jsonTest = strings.ReplaceAll(jsonTest, "\\\"", "\"") 98 // it's json we convert to heredoc back 99 var tmp interface{} = map[string]interface{}{} 100 err := json.Unmarshal([]byte(jsonTest), &tmp) 101 if err != nil { 102 tmp = make([]interface{}, 0) 103 err = json.Unmarshal([]byte(jsonTest), &tmp) 104 } 105 if err == nil { 106 dataJSONBytes, err := json.MarshalIndent(tmp, "", " ") 107 if err == nil { 108 jsonData := strings.Split(string(dataJSONBytes), "\n") 109 // first line for heredoc 110 jsonData = append([]string{lines[0]}, jsonData...) 111 // last line for heredoc 112 jsonData = append(jsonData, lines[len(lines)-1]) 113 hereDoc := strings.Join(jsonData, "\n") 114 t.Token.Text = hereDoc 115 } 116 } 117 } 118 default: 119 } 120 121 // A hack so that Assign.IsValid is true, so that the printer will output = 122 o.Assign.Line = 1 123 124 v.visit(o.Val) 125 } 126 127 func Print(data interface{}, mapsObjects map[string]struct{}, format string) ([]byte, error) { 128 switch format { 129 case "hcl": 130 return hclPrint(data, mapsObjects) 131 case "json": 132 return jsonPrint(data) 133 } 134 return []byte{}, errors.New("error: unknown output format") 135 } 136 137 func hclPrint(data interface{}, mapsObjects map[string]struct{}) ([]byte, error) { 138 dataBytesJSON, err := jsonPrint(data) 139 if err != nil { 140 return dataBytesJSON, err 141 } 142 dataJSON := string(dataBytesJSON) 143 nodes, err := hclParser.Parse([]byte(dataJSON)) 144 if err != nil { 145 log.Println(dataJSON) 146 return []byte{}, fmt.Errorf("error parsing terraform json: %v", err) 147 } 148 var sanitizer astSanitizer 149 sanitizer.visit(nodes) 150 151 var b bytes.Buffer 152 err = hclPrinter.Fprint(&b, nodes) 153 if err != nil { 154 return nil, fmt.Errorf("error writing HCL: %v", err) 155 } 156 s := b.String() 157 158 // Remove extra whitespace... 159 s = strings.ReplaceAll(s, "\n\n", "\n") 160 161 // ...but leave whitespace between resources 162 s = strings.ReplaceAll(s, "}\nresource", "}\n\nresource") 163 164 // Apply Terraform style (alignment etc.) 165 formatted, err := hclPrinter.Format([]byte(s)) 166 if err != nil { 167 return nil, err 168 } 169 // hack for support terraform 0.12 170 formatted = terraform12Adjustments(formatted, mapsObjects) 171 // hack for support terraform 0.13 172 formatted = terraform13Adjustments(formatted) 173 if err != nil { 174 log.Println("Invalid HCL follows:") 175 for i, line := range strings.Split(s, "\n") { 176 fmt.Printf("%4d|\t%s\n", i+1, line) 177 } 178 return nil, fmt.Errorf("error formatting HCL: %v", err) 179 } 180 181 return formatted, nil 182 } 183 184 func terraform12Adjustments(formatted []byte, mapsObjects map[string]struct{}) []byte { 185 singletonListFix := regexp.MustCompile(`^\s*\w+ = {`) 186 singletonListFixEnd := regexp.MustCompile(`^\s*}`) 187 188 s := string(formatted) 189 old := " = {" 190 newEquals := " {" 191 lines := strings.Split(s, "\n") 192 prefix := make([]string, 0) 193 for i, line := range lines { 194 if singletonListFixEnd.MatchString(line) && len(prefix) > 0 { 195 prefix = prefix[:len(prefix)-1] 196 continue 197 } 198 if !singletonListFix.MatchString(line) { 199 continue 200 } 201 key := strings.Trim(strings.Split(line, old)[0], " ") 202 prefix = append(prefix, key) 203 if _, exist := mapsObjects[strings.Join(prefix, ".")]; exist { 204 continue 205 } 206 lines[i] = strings.ReplaceAll(line, old, newEquals) 207 } 208 s = strings.Join(lines, "\n") 209 return []byte(s) 210 } 211 212 func terraform13Adjustments(formatted []byte) []byte { 213 s := string(formatted) 214 requiredProvidersRe := regexp.MustCompile("required_providers \".*\" {") 215 oldRequiredProviders := "\"required_providers\"" 216 newRequiredProviders := "required_providers" 217 lines := strings.Split(s, "\n") 218 for i, line := range lines { 219 if requiredProvidersRe.MatchString(line) { 220 parts := strings.Split(strings.TrimSpace(line), " ") 221 provider := strings.ReplaceAll(parts[1], "\"", "") 222 lines[i] = "\t" + newRequiredProviders + " {" 223 lines[i+1] = "\t\t" + provider + " = {\n\t" + lines[i+1] + "\n\t\t}" 224 } 225 lines[i] = strings.Replace(lines[i], oldRequiredProviders, newRequiredProviders, 1) 226 } 227 s = strings.Join(lines, "\n") 228 return []byte(s) 229 } 230 231 func escapeRune(s string) string { 232 return fmt.Sprintf("-%04X-", s) 233 } 234 235 // Sanitize name for terraform style 236 func TfSanitize(name string) string { 237 name = unsafeChars.ReplaceAllStringFunc(name, escapeRune) 238 name = "tfer--" + name 239 return name 240 } 241 242 // Print hcl file from TerraformResource + provider 243 func HclPrintResource(resources []Resource, providerData map[string]interface{}, output string) ([]byte, error) { 244 resourcesByType := map[string]map[string]interface{}{} 245 mapsObjects := map[string]struct{}{} 246 indexRe := regexp.MustCompile(`\.[0-9]+`) 247 for _, res := range resources { 248 r := resourcesByType[res.InstanceInfo.Type] 249 if r == nil { 250 r = make(map[string]interface{}) 251 resourcesByType[res.InstanceInfo.Type] = r 252 } 253 254 if r[res.ResourceName] != nil { 255 log.Println(resources) 256 log.Printf("[ERR]: duplicate resource found: %s.%s", res.InstanceInfo.Type, res.ResourceName) 257 continue 258 } 259 260 r[res.ResourceName] = res.Item 261 262 for k := range res.InstanceState.Attributes { 263 if strings.HasSuffix(k, ".%") { 264 key := strings.TrimSuffix(k, ".%") 265 mapsObjects[indexRe.ReplaceAllString(key, "")] = struct{}{} 266 } 267 } 268 } 269 270 data := map[string]interface{}{} 271 if len(resourcesByType) > 0 { 272 data["resource"] = resourcesByType 273 } 274 if len(providerData) > 0 { 275 data["provider"] = providerData 276 } 277 var err error 278 279 hclBytes, err := Print(data, mapsObjects, output) 280 if err != nil { 281 return []byte{}, err 282 } 283 return hclBytes, nil 284 }