From b28b379a7bfaff4c7cc4ae9ff9e1e57e5f7bf5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Taneli=20Lepp=C3=A4?= Date: Thu, 9 Jun 2022 11:57:51 +0200 Subject: [PATCH] Added a tool to update provider_meta/module_name, Terraform version and provider versions on batch basis. Also includes a pipeline to manually release new versions and update all module names. --- .github/workflows/release.yml | 65 +++++++++++ tools/tfeditor/go.mod | 8 ++ tools/tfeditor/go.sum | 51 +++++++++ tools/tfeditor/main.go | 198 ++++++++++++++++++++++++++++++++++ 4 files changed, 322 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 tools/tfeditor/go.mod create mode 100644 tools/tfeditor/go.sum create mode 100644 tools/tfeditor/main.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5a8755e7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +# Copyright 2022 Google LLC +# +# 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. + +name: | + Create a new release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version" + required: true + changelog: + description: "I have updated the CHANGELOG" + required: true + type: boolean + +permissions: + contents: write + +jobs: + release: + name: "Release new version" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: "Validate input" + run: | + [[ "${{ github.event.inputs.changelog }}" != "true" ]] && { echo 'You didn''t update the changelog.' ; exit 1; } + [[ -n "${{ github.event.inputs.version }}" ]] || { echo 'Version not specified!'; exit 1; } + [[ "${{ github.event.inputs.version }}" != v* ]] && { echo 'Version does not start with v!' ; exit 1; } + + - uses: actions/setup-go@v3 + with: + go-version: 1.16 + + - name: "Update all module names" + run: | + cd tools/tfeditor + go build . + ./tfeditor -path ../.. -module-name "google-pso-tool/cloud-foundation-fabric/{{ .Module }}/${{ github.event.inputs.version }}" + cd ../.. + + git config --global user.name "Release Automation" + git config --global user.email "cloud-foundation-fabric@google.com" + + git commit -a -m "Release version ${{ github.event.inputs.version }}" + git push origin master + + - name: "Tag and release" + run: | + git tag ${{ github.event.inputs.version }} + git push origin ${{ github.event.inputs.version }} diff --git a/tools/tfeditor/go.mod b/tools/tfeditor/go.mod new file mode 100644 index 00000000..d1a4a41e --- /dev/null +++ b/tools/tfeditor/go.mod @@ -0,0 +1,8 @@ +module github.com/GoogleCloudPlatform/cloud-foundation-fabric/tools/tfeditor + +go 1.16 + +require ( + github.com/hashicorp/hcl/v2 v2.12.0 // indirect + github.com/zclconf/go-cty v1.8.0 // indirect +) diff --git a/tools/tfeditor/go.sum b/tools/tfeditor/go.sum new file mode 100644 index 00000000..1ed29912 --- /dev/null +++ b/tools/tfeditor/go.sum @@ -0,0 +1,51 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= +github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/hashicorp/hcl/v2 v2.12.0 h1:PsYxySWpMD4KPaoJLnsHwtK5Qptvj/4Q6s0t4sUxZf4= +github.com/hashicorp/hcl/v2 v2.12.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= +github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= +github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tools/tfeditor/main.go b/tools/tfeditor/main.go new file mode 100644 index 00000000..f8a4e0a9 --- /dev/null +++ b/tools/tfeditor/main.go @@ -0,0 +1,198 @@ +/* + Copyright 2022 Google LLC + + 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. + + This tool updates in-place versions.tf files to change module_name parameter + on an automated basis. In retains all existing comments and structures. +*/ +package main + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" +) + +type Provider struct { + Source string + Version string +} + +func main() { + var path string + var pattern string + var moduleName string + var terraformVersion string + var providerVersions string + var updateProviderVersions bool = false + var updateTerraformVersion bool = false + var updateModuleName bool = false + + flag.StringVar(&path, "path", "./", "Path to search for file pattern (default ./") + flag.StringVar(&pattern, "pattern", "versions.tf", "Pattern to search (default versions.tf)") + flag.StringVar(&moduleName, "module-name", "", "module_name attribute for provider_meta (can be templated with {{ .Module }})") + flag.StringVar(&providerVersions, "provider-versions", "", "set provider versions (usage: hashicorp/google>= 4.0.0,hashicorp/googlegoogle-beta== 4.0.0)") + flag.StringVar(&terraformVersion, "terraform-version", "", "Update terraform required_version") + flag.Parse() + + if !strings.HasSuffix(path, "/") { + path = fmt.Sprintf("%s/", path) + } + + if moduleName != "" { + updateModuleName = true + log.Printf("Updating module_name to: %s", moduleName) + } + + if terraformVersion != "" { + updateTerraformVersion = true + log.Printf("Updating Terraform version to: %s", terraformVersion) + } + + providerVersionsMap := map[string]Provider{} + if providerVersions != "" { + for _, v := range strings.Split(providerVersions, ",") { + re := regexp.MustCompile("([a-zA-Z_/-]+)(.+)") + split := re.FindAllStringSubmatch(v, -1) + for _, splitN := range split { + providerKey := filepath.Base(splitN[1]) + providerVersionsMap[providerKey] = Provider{Source: splitN[1], Version: splitN[2]} + log.Printf("Updating provider %s to: %s %s", providerKey, providerVersionsMap[providerKey].Source, providerVersionsMap[providerKey].Version) + } + } + updateProviderVersions = true + } + + log.Printf("Looking for files (%s) in: %s", pattern, path) + + var foundFiles []string + err := filepath.Walk(path, func(file string, f os.FileInfo, err error) error { + if isMatch, _ := filepath.Match(pattern, filepath.Base(file)); isMatch { + foundFiles = append(foundFiles, file) + } + return nil + }) + log.Printf("Found %d files.", len(foundFiles)) + + for _, foundFile := range foundFiles { + fileBytes, fErr := ioutil.ReadFile(foundFile) + if fErr != nil { + panic(fErr) + } + fileBasename := filepath.Base(foundFile) + + hclFile, diag := hclwrite.ParseConfig(fileBytes, fileBasename, hcl.Pos{}) + if diag == nil { + hclBody := hclFile.Body() + for _, block := range hclBody.Blocks() { + if block.Type() == "terraform" { + if updateTerraformVersion { + for k, _ := range block.Body().Attributes() { + if k == "required_version" { + block.Body().SetAttributeValue("required_version", cty.StringVal(terraformVersion)) + } + } + } + + hasProviderMetaForGoogle := false + hasProviderMetaForGoogleBeta := false + + // Expand template + tmpl, tErr := template.New("modulename").Parse(moduleName) + if tErr != nil { + panic(tErr) + } + expandedBuffer := new(bytes.Buffer) + tErr = tmpl.Execute(expandedBuffer, map[string]string{ + "Module": filepath.Base(filepath.Dir(foundFile)), + }) + if tErr != nil { + panic(tErr) + } + expandedModuleName := expandedBuffer.String() + + for _, tfBlock := range block.Body().Blocks() { + if tfBlock.Type() == "required_providers" && updateProviderVersions { + for k, _ := range tfBlock.Body().Attributes() { + if provider, ok := providerVersionsMap[k]; ok { + tfBlock.Body().SetAttributeValue(k, cty.ObjectVal(map[string]cty.Value{ + "source": cty.StringVal(provider.Source), + "version": cty.StringVal(provider.Version), + })) + } + } + } + + if tfBlock.Type() == "provider_meta" && updateModuleName { + labels := tfBlock.Labels() + if len(labels) > 0 { + if labels[0] == "google" { + hasProviderMetaForGoogle = true + tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + } + if labels[0] == "google-beta" { + hasProviderMetaForGoogleBeta = true + tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + } + } + } + } + + if updateModuleName { + if !hasProviderMetaForGoogle { + providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google"}) + providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + block.Body().AppendBlock(providerMetaGoogleBlock) + } + if !hasProviderMetaForGoogleBeta { + providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google-beta"}) + providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + block.Body().AppendBlock(providerMetaGoogleBlock) + } + } + } + } + + log.Printf("Updating: %s", foundFile) + info, sErr := os.Stat(foundFile) + if sErr != nil { + panic(sErr) + } + tempFilePath := fmt.Sprintf("%s.tmp", foundFile) + wErr := os.WriteFile(tempFilePath, hclFile.Bytes(), info.Mode()) + if wErr != nil { + panic(wErr) + } + os.Rename(tempFilePath, foundFile) + } else { + panic(diag) + } + } + + if err != nil { + panic(err) + } + log.Printf("All done.") +}