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  }