|
| 1 | +/* |
| 2 | + Copyright 2022 Google LLC |
| 3 | +
|
| 4 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + you may not use this file except in compliance with the License. |
| 6 | + You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | + Unless required by applicable law or agreed to in writing, software |
| 11 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + See the License for the specific language governing permissions and |
| 14 | + limitations under the License. |
| 15 | +
|
| 16 | + This tool updates in-place versions.tf files to change module_name parameter |
| 17 | + on an automated basis. In retains all existing comments and structures. |
| 18 | +*/ |
| 19 | +package main |
| 20 | + |
| 21 | +import ( |
| 22 | + "bytes" |
| 23 | + "flag" |
| 24 | + "fmt" |
| 25 | + "io/ioutil" |
| 26 | + "log" |
| 27 | + "os" |
| 28 | + "path/filepath" |
| 29 | + "regexp" |
| 30 | + "strings" |
| 31 | + "text/template" |
| 32 | + |
| 33 | + "github.com/hashicorp/hcl/v2" |
| 34 | + "github.com/hashicorp/hcl/v2/hclwrite" |
| 35 | + "github.com/zclconf/go-cty/cty" |
| 36 | +) |
| 37 | + |
| 38 | +type Provider struct { |
| 39 | + Source string |
| 40 | + Version string |
| 41 | +} |
| 42 | + |
| 43 | +func main() { |
| 44 | + var path string |
| 45 | + var pattern string |
| 46 | + var moduleName string |
| 47 | + var terraformVersion string |
| 48 | + var providerVersions string |
| 49 | + var updateProviderVersions bool = false |
| 50 | + var updateTerraformVersion bool = false |
| 51 | + var updateModuleName bool = false |
| 52 | + |
| 53 | + flag.StringVar(&path, "path", "./", "Path to search for file pattern (default ./") |
| 54 | + flag.StringVar(&pattern, "pattern", "versions.tf", "Pattern to search (default versions.tf)") |
| 55 | + flag.StringVar(&moduleName, "module-name", "", "module_name attribute for provider_meta (can be templated with {{ .Module }})") |
| 56 | + flag.StringVar(&providerVersions, "provider-versions", "", "set provider versions (usage: hashicorp/google>= 4.0.0,hashicorp/googlegoogle-beta== 4.0.0)") |
| 57 | + flag.StringVar(&terraformVersion, "terraform-version", "", "Update terraform required_version") |
| 58 | + flag.Parse() |
| 59 | + |
| 60 | + if !strings.HasSuffix(path, "/") { |
| 61 | + path = fmt.Sprintf("%s/", path) |
| 62 | + } |
| 63 | + |
| 64 | + if moduleName != "" { |
| 65 | + updateModuleName = true |
| 66 | + log.Printf("Updating module_name to: %s", moduleName) |
| 67 | + } |
| 68 | + |
| 69 | + if terraformVersion != "" { |
| 70 | + updateTerraformVersion = true |
| 71 | + log.Printf("Updating Terraform version to: %s", terraformVersion) |
| 72 | + } |
| 73 | + |
| 74 | + providerVersionsMap := map[string]Provider{} |
| 75 | + if providerVersions != "" { |
| 76 | + for _, v := range strings.Split(providerVersions, ",") { |
| 77 | + re := regexp.MustCompile("([a-zA-Z_/-]+)(.+)") |
| 78 | + split := re.FindAllStringSubmatch(v, -1) |
| 79 | + for _, splitN := range split { |
| 80 | + providerKey := filepath.Base(splitN[1]) |
| 81 | + providerVersionsMap[providerKey] = Provider{Source: splitN[1], Version: splitN[2]} |
| 82 | + log.Printf("Updating provider %s to: %s %s", providerKey, providerVersionsMap[providerKey].Source, providerVersionsMap[providerKey].Version) |
| 83 | + } |
| 84 | + } |
| 85 | + updateProviderVersions = true |
| 86 | + } |
| 87 | + |
| 88 | + log.Printf("Looking for files (%s) in: %s", pattern, path) |
| 89 | + |
| 90 | + var foundFiles []string |
| 91 | + err := filepath.Walk(path, func(file string, f os.FileInfo, err error) error { |
| 92 | + if isMatch, _ := filepath.Match(pattern, filepath.Base(file)); isMatch { |
| 93 | + foundFiles = append(foundFiles, file) |
| 94 | + } |
| 95 | + return nil |
| 96 | + }) |
| 97 | + log.Printf("Found %d files.", len(foundFiles)) |
| 98 | + |
| 99 | + for _, foundFile := range foundFiles { |
| 100 | + fileBytes, fErr := ioutil.ReadFile(foundFile) |
| 101 | + if fErr != nil { |
| 102 | + panic(fErr) |
| 103 | + } |
| 104 | + fileBasename := filepath.Base(foundFile) |
| 105 | + |
| 106 | + hclFile, diag := hclwrite.ParseConfig(fileBytes, fileBasename, hcl.Pos{}) |
| 107 | + if diag == nil { |
| 108 | + hclBody := hclFile.Body() |
| 109 | + for _, block := range hclBody.Blocks() { |
| 110 | + if block.Type() == "terraform" { |
| 111 | + if updateTerraformVersion { |
| 112 | + for k, _ := range block.Body().Attributes() { |
| 113 | + if k == "required_version" { |
| 114 | + block.Body().SetAttributeValue("required_version", cty.StringVal(terraformVersion)) |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + hasProviderMetaForGoogle := false |
| 120 | + hasProviderMetaForGoogleBeta := false |
| 121 | + |
| 122 | + // Expand template |
| 123 | + tmpl, tErr := template.New("modulename").Parse(moduleName) |
| 124 | + if tErr != nil { |
| 125 | + panic(tErr) |
| 126 | + } |
| 127 | + expandedBuffer := new(bytes.Buffer) |
| 128 | + tErr = tmpl.Execute(expandedBuffer, map[string]string{ |
| 129 | + "Module": filepath.Base(filepath.Dir(foundFile)), |
| 130 | + }) |
| 131 | + if tErr != nil { |
| 132 | + panic(tErr) |
| 133 | + } |
| 134 | + expandedModuleName := expandedBuffer.String() |
| 135 | + |
| 136 | + for _, tfBlock := range block.Body().Blocks() { |
| 137 | + if tfBlock.Type() == "required_providers" && updateProviderVersions { |
| 138 | + for k, _ := range tfBlock.Body().Attributes() { |
| 139 | + if provider, ok := providerVersionsMap[k]; ok { |
| 140 | + tfBlock.Body().SetAttributeValue(k, cty.ObjectVal(map[string]cty.Value{ |
| 141 | + "source": cty.StringVal(provider.Source), |
| 142 | + "version": cty.StringVal(provider.Version), |
| 143 | + })) |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + if tfBlock.Type() == "provider_meta" && updateModuleName { |
| 149 | + labels := tfBlock.Labels() |
| 150 | + if len(labels) > 0 { |
| 151 | + if labels[0] == "google" { |
| 152 | + hasProviderMetaForGoogle = true |
| 153 | + tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) |
| 154 | + } |
| 155 | + if labels[0] == "google-beta" { |
| 156 | + hasProviderMetaForGoogleBeta = true |
| 157 | + tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + if updateModuleName { |
| 164 | + if !hasProviderMetaForGoogle { |
| 165 | + providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google"}) |
| 166 | + providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) |
| 167 | + block.Body().AppendBlock(providerMetaGoogleBlock) |
| 168 | + } |
| 169 | + if !hasProviderMetaForGoogleBeta { |
| 170 | + providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google-beta"}) |
| 171 | + providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) |
| 172 | + block.Body().AppendBlock(providerMetaGoogleBlock) |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + log.Printf("Updating: %s", foundFile) |
| 179 | + info, sErr := os.Stat(foundFile) |
| 180 | + if sErr != nil { |
| 181 | + panic(sErr) |
| 182 | + } |
| 183 | + tempFilePath := fmt.Sprintf("%s.tmp", foundFile) |
| 184 | + wErr := os.WriteFile(tempFilePath, hclFile.Bytes(), info.Mode()) |
| 185 | + if wErr != nil { |
| 186 | + panic(wErr) |
| 187 | + } |
| 188 | + os.Rename(tempFilePath, foundFile) |
| 189 | + } else { |
| 190 | + panic(diag) |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + if err != nil { |
| 195 | + panic(err) |
| 196 | + } |
| 197 | + log.Printf("All done.") |
| 198 | +} |
0 commit comments