diff --git a/.envrc.sample b/.envrc.sample index 37b1096..52da9b6 100644 --- a/.envrc.sample +++ b/.envrc.sample @@ -1 +1 @@ -export GO111MODULE=on +export TERRAFORM_VERSION=1.5.2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 96457ca..9a983dd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,3 +24,21 @@ jobs: go-version-file: '.go-version' - name: test run: make test + testacc: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + terraform: + - 1.5.2 + - 0.14.11 + env: + TERRAFORM_VERSION: ${{ matrix.terraform }} + steps: + - uses: actions/checkout@v3 + - name: docker build + run: docker-compose build + - name: terraform --version + run: docker compose run --rm tfupdate terraform --version + - name: testacc + run: docker compose run --rm tfupdate make testacc diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..cb7f466 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,27 @@ +# terraform +ARG TERRAFORM_VERSION=latest +FROM hashicorp/terraform:$TERRAFORM_VERSION AS terraform + +# tfupdate +FROM golang:1.20-alpine3.18 AS tfupdate +RUN apk --no-cache add make git + +# A workaround for a permission issue of git. +# Since UIDs are different between host and container, +# the .git directory is untrusted by default. +# We need to allow it explicitly. +# https://github.com/actions/checkout/issues/760 +RUN git config --global --add safe.directory /work + +# for testing +RUN apk add --no-cache bash +COPY --from=terraform /bin/terraform /usr/local/bin/ + +WORKDIR /work + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN make install + diff --git a/Makefile b/Makefile index 812b511..86683b4 100644 --- a/Makefile +++ b/Makefile @@ -22,5 +22,20 @@ lint: test: build go test ./... +.PHONY: testacc +testacc: install testacc-lock-simple + +.PHONY: testacc-lock-simple +testacc-lock-simple: install + scripts/testacc/lock.sh run simple + +.PHONY: testacc-lock-debug +testacc-lock-debug: install + scripts/testacc/lock.sh $(ARG) + +.PHONY: testacc-all +testacc-all: install + scripts/testacc/all.sh + .PHONY: check check: lint test diff --git a/README.md b/README.md index c03311c..ad815f8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ ## Features - Update version constraints of Terraform core, providers, and modules -- Update all your Terraform configurations recursively under a given directory +- Update dependency lock files (.terraform.lock.hcl) without Terraform CLI +- Update all your Terraform configurations and lock files recursively under a given directory - Get the latest release version from the GitHub, GitLab, or Terraform Registry - Terraform v0.12+ support @@ -64,10 +65,11 @@ $ docker run -it --rm minamijoyo/tfupdate --version ## Usage ``` -tfupdate --help +$ tfupdate --help Usage: tfupdate [--version] [--help] [] Available commands are: + lock Update dependency lock files module Update version constraints for module provider Update version constraints for provider release Get release version information @@ -203,6 +205,8 @@ terraform { } ``` +For updating the dependency lock file (.terraform.lock.hcl), use the `tfupdate lock` command. + ### module ``` @@ -358,6 +362,136 @@ $ tfupdate release list -n 5 hashicorp/terraform 0.12.21 ``` +### lock + +The tfupdate lock command updates the dependency lock file (.terraform.lock.hcl). +For more information on the dependency lock file, see the official Terraform documentation: +https://developer.hashicorp.com/terraform/language/files/dependency-lock + +``` +$ tfupdate lock --help +Usage: tfupdate lock [options] + +Arguments + PATH A path of directory to update + +Options: + --platform Specify a platform to update dependency lock files. + At least one or more --platform flags must be specified. + Use this option multiple times to include checksums for multiple target systems. + Target platform names consist of an operating system and a CPU architecture. + (e.g. linux_amd64, darwin_amd64, darwin_arm64) + -r --recursive Check a directory recursively (default: false) + -i --ignore-path A regular expression for path to ignore + If you want to ignore multiple directories, set the flag multiple times. +``` + +Given the following configuration: + +``` +$ cat test-fixtures/lock/simple/main.tf +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.1.1" + } + } +} +``` + +As you know, you can generate the dependency lock file by the terraform providers lock command: + +``` +$ terraform -chdir=test-fixtures/lock/simple providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64 +``` + +``` +$ cat test-fixtures/lock/simple/.terraform.lock.hcl +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", + "h1:Pctug/s/2Hg5FJqjYcTM0kPyx3AoYK1MpRWO0T9V2ns=", + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", + ] +} +``` + +When updating provider version, the lock file must also be updated: + +``` +$ tfupdate provider null -v 3.2.1 ./test-fixtures/lock/simple/ +``` + +``` +$ cat test-fixtures/lock/simple/main.tf +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.2.1" + } + } +} +``` + +You can update the lock file by the tfupdate lock command without Terraform CLI: + +``` +$ tfupdate lock --platform=linux_amd64 --platform=darwin_amd64 --platform=darwin_arm64 ./test-fixtures/lock/simple/ +``` + +Note that unlike the terraform providers lock command, the `--platform` flag requires two hyphens. + +``` +$ cat test-fixtures/lock/simple/.terraform.lock.hcl +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} +``` + +The tfupdate lock command parses the `required_providers` block in your configuration, downloads provider packages and calculates hash values under the hood. The most important point is that it caches calculated hash values in memory, which gives us a huge performance advantage when updating multiple directories at once using the `-r (--recursive)` option. + +To skip terraform init, we assume that all dependencies are pinned to a specific version in the required_providers block of the root module. Note that version constraint expressions or indirect dependencies via modules are not supported and ignored. + ## Keep your dependencies up-to-date If you integrate tfupdate with your favorite CI or job scheduler, you can check the latest release daily and create a Pull Request automatically. diff --git a/command/lock.go b/command/lock.go new file mode 100644 index 0000000..5a46151 --- /dev/null +++ b/command/lock.go @@ -0,0 +1,94 @@ +package command + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/minamijoyo/tfupdate/tfupdate" + flag "github.com/spf13/pflag" +) + +// LockCommand is a command which update dependency lock files. +type LockCommand struct { + Meta + platforms []string + path string + recursive bool + ignorePaths []string +} + +// Run runs the procedure of this command. +func (c *LockCommand) Run(args []string) int { + cmdFlags := flag.NewFlagSet("lock", flag.ContinueOnError) + cmdFlags.StringArrayVar(&c.platforms, "platform", []string{}, "A target platform for dependecy lock file") + cmdFlags.BoolVarP(&c.recursive, "recursive", "r", false, "Check a directory recursively") + cmdFlags.StringArrayVarP(&c.ignorePaths, "ignore-path", "i", []string{}, "A regular expression for path to ignore") + + if err := cmdFlags.Parse(args); err != nil { + c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err)) + return 1 + } + + if len(cmdFlags.Args()) != 1 { + c.UI.Error(fmt.Sprintf("The command expects 1 arguments, but got %d", len(cmdFlags.Args()))) + c.UI.Error(c.Help()) + return 1 + } + + c.path = cmdFlags.Arg(0) + + if len(c.platforms) == 0 { + c.UI.Error("The --platform flag is required") + c.UI.Error(c.Help()) + return 1 + } + + log.Println("[INFO] Update dependency lock files") + option, err := tfupdate.NewOption("lock", "", "", c.platforms, c.recursive, c.ignorePaths) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + gc, err := tfupdate.NewGlobalContext(c.Fs, option) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + err = tfupdate.UpdateFileOrDir(context.Background(), gc, c.path) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + return 0 +} + +// Help returns long-form help text. +func (c *LockCommand) Help() string { + helpText := ` +Usage: tfupdate lock [options] + +Arguments + PATH A path of directory to update + +Options: + --platform Specify a platform to update dependency lock files. + At least one or more --platform flags must be specified. + Use this option multiple times to include checksums for multiple target systems. + Target platform names consist of an operating system and a CPU architecture. + (e.g. linux_amd64, darwin_amd64, darwin_arm64) + -r --recursive Check a directory recursively (default: false) + -i --ignore-path A regular expression for path to ignore + If you want to ignore multiple directories, set the flag multiple times. +` + return strings.TrimSpace(helpText) +} + +// Synopsis returns one-line help text. +func (c *LockCommand) Synopsis() string { + return "Update dependency lock files" +} diff --git a/command/module.go b/command/module.go index f4056fa..5a7a655 100644 --- a/command/module.go +++ b/command/module.go @@ -1,6 +1,7 @@ package command import ( + "context" "fmt" "log" "strings" @@ -49,13 +50,19 @@ func (c *ModuleCommand) Run(args []string) int { } log.Printf("[INFO] Update module %s to %s", c.name, v) - option, err := tfupdate.NewOption("module", c.name, v, c.recursive, c.ignorePaths) + option, err := tfupdate.NewOption("module", c.name, v, []string{}, c.recursive, c.ignorePaths) if err != nil { c.UI.Error(err.Error()) return 1 } - err = tfupdate.UpdateFileOrDir(c.Fs, c.path, option) + gc, err := tfupdate.NewGlobalContext(c.Fs, option) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + err = tfupdate.UpdateFileOrDir(context.Background(), gc, c.path) if err != nil { c.UI.Error(err.Error()) return 1 diff --git a/command/provider.go b/command/provider.go index 888ddd3..525bfa9 100644 --- a/command/provider.go +++ b/command/provider.go @@ -59,13 +59,19 @@ func (c *ProviderCommand) Run(args []string) int { } log.Printf("[INFO] Update provider %s to %s", c.name, v) - option, err := tfupdate.NewOption("provider", c.name, v, c.recursive, c.ignorePaths) + option, err := tfupdate.NewOption("provider", c.name, v, []string{}, c.recursive, c.ignorePaths) if err != nil { c.UI.Error(err.Error()) return 1 } - err = tfupdate.UpdateFileOrDir(c.Fs, c.path, option) + gc, err := tfupdate.NewGlobalContext(c.Fs, option) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + err = tfupdate.UpdateFileOrDir(context.Background(), gc, c.path) if err != nil { c.UI.Error(err.Error()) return 1 diff --git a/command/terraform.go b/command/terraform.go index c19081a..e925333 100644 --- a/command/terraform.go +++ b/command/terraform.go @@ -56,13 +56,19 @@ func (c *TerraformCommand) Run(args []string) int { } log.Printf("[INFO] Update terraform to %s", v) - option, err := tfupdate.NewOption("terraform", "", v, c.recursive, c.ignorePaths) + option, err := tfupdate.NewOption("terraform", "", v, []string{}, c.recursive, c.ignorePaths) if err != nil { c.UI.Error(err.Error()) return 1 } - err = tfupdate.UpdateFileOrDir(c.Fs, c.path, option) + gc, err := tfupdate.NewGlobalContext(c.Fs, option) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + err = tfupdate.UpdateFileOrDir(context.Background(), gc, c.path) if err != nil { c.UI.Error(err.Error()) return 1 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..59e1305 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,11 @@ +services: + tfupdate: + build: + context: . + dockerfile: ./Dockerfile.dev + args: + TERRAFORM_VERSION: ${TERRAFORM_VERSION:-latest} + volumes: + - ".:/work" + environment: + CGO_ENABLED: 0 # disable cgo for go test diff --git a/go.mod b/go.mod index b674e56..9397112 100644 --- a/go.mod +++ b/go.mod @@ -4,38 +4,45 @@ go 1.20 require ( github.com/davecgh/go-spew v1.1.1 + github.com/google/go-cmp v0.5.9 github.com/google/go-github/v28 v28.1.1 - github.com/hashicorp/go-version v1.3.0 + github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl/v2 v2.17.0 github.com/hashicorp/logutils v1.0.0 + github.com/hashicorp/terraform-config-inspect v0.0.0-20230614215431-f32df32a01cd + github.com/hashicorp/terraform-registry-address v0.2.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/mitchellh/cli v1.0.0 - github.com/pkg/errors v0.8.1 - github.com/spf13/afero v1.2.2 + github.com/pkg/errors v0.9.1 + github.com/spf13/afero v1.9.5 github.com/spf13/pflag v1.0.5 github.com/xanzy/go-gitlab v0.20.1 github.com/zclconf/go-cty v1.13.0 - golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 + golang.org/x/mod v0.6.0 + golang.org/x/oauth2 v0.4.0 ) require ( - github.com/agext/levenshtein v1.2.1 // indirect + github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/fatih/color v1.7.0 // indirect - github.com/golang/protobuf v1.3.4 // indirect - github.com/google/go-cmp v0.3.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f // indirect + github.com/hashicorp/terraform-svchost v0.0.1 // indirect github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.3 // indirect - github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/posener/complete v1.1.1 // indirect - golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 // indirect - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/text v0.3.8 // indirect - google.golang.org/appengine v1.6.5 // indirect + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/net v0.5.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index 284595e..9d79e67 100644 --- a/go.sum +++ b/go.sum @@ -1,97 +1,527 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= +github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 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/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.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 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 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/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= -github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= +github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/terraform-config-inspect v0.0.0-20230614215431-f32df32a01cd h1:1uPcotqoL4TjcGKlgIe7OFSRplf7BMVtUjekwmCrvuM= +github.com/hashicorp/terraform-config-inspect v0.0.0-20230614215431-f32df32a01cd/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg= +github.com/hashicorp/terraform-registry-address v0.2.0 h1:92LUg03NhfgZv44zpNTLBGIbiyTokQCDcdH5BhVHT3s= +github.com/hashicorp/terraform-registry-address v0.2.0/go.mod h1:478wuzJPzdmqT6OGbB/iH82EDcI8VFM4yujknh/1nIs= +github.com/hashicorp/terraform-svchost v0.0.1 h1:Zj6fR5wnpOHnJUmLyWozjMeDaVuE+cstMPj41/eKmSQ= +github.com/hashicorp/terraform-svchost v0.0.1/go.mod h1:ut8JaH0vumgdCfJaihdcZULqkAwHdQNwNH7taIDdsZM= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xanzy/go-gitlab v0.20.1 h1:+1BWDry84G5PzsnzG9DI4YjPbHeWKyouM0q0gfDPKgY= github.com/xanzy/go-gitlab v0.20.1/go.mod h1:LSfUQ9OPDnwRqulJk2HcWaAiFfCzaknyeGvjQI67MbE= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 h1:O8uGbHCqlTp2P6QJSLmCojM4mN6UemYv8K+dCnmHmu0= -golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/lock/hash.go b/lock/hash.go new file mode 100644 index 0000000..d9c3bc2 --- /dev/null +++ b/lock/hash.go @@ -0,0 +1,80 @@ +package lock + +import ( + "fmt" + "os" + "strings" + + "golang.org/x/mod/sumdb/dirhash" +) + +// zipDataToH1Hash is a helper function that calculates the h1 hash value from +// bytes sequence of the provider's zip archive. +func zipDataToH1Hash(zipData []byte) (string, error) { + tmpZipfile, err := writeTempFile(zipData) + if err != nil { + return "", err + } + defer os.Remove(tmpZipfile.Name()) + + // The h1 hash value in .terraform.lock.hcl uses the same hash function as go.sum. + hash, err := dirhash.HashZip(tmpZipfile.Name(), dirhash.Hash1) + if err != nil { + return "", fmt.Errorf("failed to calculate h1 hash: %s", err) + } + + return hash, nil +} + +// writeTempFile writes content to a temporary file and return its file. +func writeTempFile(content []byte) (*os.File, error) { + tmpfile, err := os.CreateTemp("", "tmp") + if err != nil { + return tmpfile, fmt.Errorf("failed to create temporary file: %s", err) + } + + if _, err := tmpfile.Write(content); err != nil { + return tmpfile, fmt.Errorf("failed to write temporary file: %s", err) + } + + if err := tmpfile.Close(); err != nil { + return tmpfile, fmt.Errorf("failed to close temporary file: %s", err) + } + + return tmpfile, nil +} + +// shaSumsDataToZhHash is a helper function for parsing zh hash values from +// bytes sequence of the shaSumsData document. +func shaSumsDataToZhHash(shaSumsData []byte) (map[string]string, error) { + document := string(shaSumsData) + zh := make(map[string]string) + // Read an entry per line. + for _, line := range strings.Split(document, "\n") { + // We expect that blank lines are not normally included, but to make the + // test data easier to read, ignore blank lines. + if len(line) == 0 { + continue + } + + // Split rows into columns with spaces, but note that there are two spaces between the columns. + // e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2 terraform-provider-null_3.2.1_darwin_arm64.zip + + fields := strings.Fields(line) + if len(fields) != 2 { + return nil, fmt.Errorf("failed to parse hash in shaSumsData: %s", document) + } + hash := fields[0] + + // Initially, we thought of using the key of the zh hash as the platform, + // but we found out that it also includes metadata such as manifest.json, + // so we decided to use the filename as it is. + filename := fields[1] + + // As the implementation of the h1 hash includes a prefix for the "h1:" + // scheme, zh also includes the "zh:" prefix for consistency. + zh[filename] = "zh:" + hash + } + + return zh, nil +} diff --git a/lock/hash_test.go b/lock/hash_test.go new file mode 100644 index 0000000..9224b0f --- /dev/null +++ b/lock/hash_test.go @@ -0,0 +1,162 @@ +package lock + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" +) + +func TestZipDataToH1Hash(t *testing.T) { + filename := "terraform-provider-dummy_v3.2.1_x5" + cases := []struct { + desc string + makeZip bool + filename string + // Actually it's a binary of the provider's executable, but here we'll use dummy data for testing. + contents string + want string + ok bool + }{ + { + desc: "darwin_arm64", + makeZip: true, + contents: "dummy_3.2.1_darwin_arm64", + want: "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + ok: true, + }, + { + desc: "darwin_amd64", + makeZip: true, + contents: "dummy_3.2.1_darwin_amd64", + want: "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + ok: true, + }, + { + desc: "linux_amd64", + makeZip: true, + contents: "dummy_3.2.1_linux_amd64", + want: "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + ok: true, + }, + { + desc: "invalid zip format", + makeZip: false, + contents: "dummy_3.2.1_linux_amd64", + want: "", + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + var zipData []byte + var err error + if tc.makeZip { + // create a zip file in memory. + zipData, err = newMockZipData(filename, tc.contents) + if err != nil { + t.Fatalf("failed to create a zip file in memory: err = %s", err) + } + } else { + // invalid zip format + zipData = []byte(tc.contents) + } + + got, err := zipDataToH1Hash(zipData) + + if tc.ok && err != nil { + t.Fatalf("failed to call zipDataToH1Hash: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: got = %s", got) + } + + if got != tc.want { + t.Errorf("got=%s, but want=%s", got, tc.want) + } + }) + } +} + +func TestShaSumsDataToZhHash(t *testing.T) { + // create a valid dummy shaSumsData. + platforms := []string{"darwin_arm64", "darwin_amd64", "linux_amd64", "windows_amd64"} + shaSumsData, err := newMockShaSumsData("dummy", "3.2.1", platforms) + if err != nil { + t.Fatalf("failed to create a shaSumsData: err = %s", err) + } + + // To update the following static test case, uncomment out here. + // t.Logf("%s", string(shaSumsData)) + + cases := []struct { + desc string + shaSumsData []byte + want map[string]string + ok bool + }{ + { + desc: "static", + // The input shaSumsData should be the same as the following dynamic + // case, but the output of newMockShaSumsData is pasted into the test + // case for test case readability. + shaSumsData: []byte(` +5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086 terraform-provider-dummy_3.2.1_darwin_arm64.zip +8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e terraform-provider-dummy_3.2.1_windows_amd64.zip +c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2 terraform-provider-dummy_3.2.1_linux_amd64.zip +fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2 terraform-provider-dummy_3.2.1_darwin_amd64.zip +`), + want: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + ok: true, + }, + { + desc: "dynamic", + shaSumsData: shaSumsData, + want: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + ok: true, + }, + { + desc: "empty", + shaSumsData: []byte(""), + want: map[string]string{}, + ok: true, + }, + { + desc: "parse hash error", + shaSumsData: []byte("aaa"), + want: nil, + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + + got, err := shaSumsDataToZhHash(tc.shaSumsData) + + if tc.ok && err != nil { + t.Fatalf("failed to call shaSumsDataToZhHash: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: got = %s", spew.Sdump(got)) + } + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.want), diff) + } + }) + } +} diff --git a/lock/index.go b/lock/index.go new file mode 100644 index 0000000..6fc54b4 --- /dev/null +++ b/lock/index.go @@ -0,0 +1,209 @@ +package lock + +import ( + "context" + "fmt" + "log" + "strings" + + tfaddr "github.com/hashicorp/terraform-registry-address" +) + +// Index is an in-memory data store for caching provider hash values. +type Index interface { + // GetOrCreateProviderVersion returns a cached provider version if available, + // otherwise creates it. + // address is a provider address such as hashicorp/null. + // version is a version number such as 3.2.1. + // platforms is a list of target platforms to generate hash values. + // Target platform names consist of an operating system and a CPU architecture such as darwin_arm64. + GetOrCreateProviderVersion(ctx context.Context, address string, version string, platforms []string) (*ProviderVersion, error) +} + +// index is an implementation for Index interface. +type index struct { + // providers is a dictionary of providerIndex. + // The key is a provider address such as hashicorp/null. + providers map[string]*providerIndex + + // papi is a ProviderDownloaderAPI interface implementation used for downloading provider. + papi ProviderDownloaderAPI +} + +// NewDefaultIndex returns a new instance of default Index. +func NewDefaultIndex() (Index, error) { + client, err := NewProviderDownloaderClient(TFRegistryConfig{}) + if err != nil { + return nil, err + } + + index := NewIndex(client) + + return index, nil +} + +// NewIndex returns a new instance of Index. +func NewIndex(papi ProviderDownloaderAPI) Index { + providers := make(map[string]*providerIndex) + return &index{ + providers: providers, + papi: papi, + } +} + +// GetOrCreateProviderVersion returns a cached provider version if available, +// otherwise creates it. +func (i *index) GetOrCreateProviderVersion(ctx context.Context, address string, version string, platforms []string) (*ProviderVersion, error) { + pi, ok := i.providers[address] + if !ok { + // cache miss + pi = newProviderIndex(address, i.papi) + i.providers[address] = pi + } + // Delegate to ProviderIndex. + return pi.getOrCreateProviderVersion(ctx, version, platforms) +} + +// The providerIndex holds multiple version data for a specific provider. +type providerIndex struct { + // address is a provider address such as hashicorp/null. + address string + + // versions is a dictionary of ProviderVersion. + // The key is a version number such as 3.2.1. + versions map[string]*ProviderVersion + + // papi is a ProviderDownloaderAPI interface implementation used for downloading provider. + papi ProviderDownloaderAPI +} + +// newProviderIndex returns a new instance of providerIndex. +func newProviderIndex(address string, papi ProviderDownloaderAPI) *providerIndex { + versions := make(map[string]*ProviderVersion) + return &providerIndex{ + address: address, + versions: versions, + papi: papi, + } +} + +// getOrCreateProviderVersion returns a cached provider version if available, +// otherwise creates it. +func (pi *providerIndex) getOrCreateProviderVersion(ctx context.Context, version string, platforms []string) (*ProviderVersion, error) { + pv, ok := pi.versions[version] + if !ok { + // cache miss + var err error + pv, err = pi.createProviderVersion(ctx, version, platforms) + if err != nil { + return nil, err + } + pi.versions[version] = pv + } + return pv, nil +} + +// createProviderVersion downloads the specified provider, calculates the hash +// value and returns an instance of the ProviderVersion. +func (pi *providerIndex) createProviderVersion(ctx context.Context, version string, platforms []string) (*ProviderVersion, error) { + ret := newEmptyProviderVersion(pi.address, version) + + for _, platform := range platforms { + req, err := newProviderDownloadRequest(pi.address, version, platform) + if err != nil { + return nil, err + } + + // Download a given provider from registry. + log.Printf("[DEBUG] providerIndex.createProviderVersion: %s, %s, %s", pi.address, version, platform) + res, err := pi.papi.ProviderDownload(ctx, req) + if err != nil { + return nil, err + } + + // Currently the Terraform Registry returns the zh hash for all platforms, + // but not the h1 hash, so the h1 hash has to be calculated separately. + // We need to calculate the values for each platform and merge the results. + pv, err := buildProviderVersion(pi.address, version, platform, res) + if err != nil { + return nil, err + } + + err = ret.Merge(pv) + if err != nil { + return nil, err + } + } + + return ret, nil +} + +// newProviderDownloadRequest is a helper function for building the parameters for downloading provider. +// address is a provider address such as hashicorp/null. +// version is a version number such as 3.2.1. +// platform is a target platform name such as darwin_arm64. +func newProviderDownloadRequest(address string, version string, platform string) (*ProviderDownloadRequest, error) { + // We parse an provider address by using the terraform-registry-address + // library to support fully qualified addresses such as + // registry.terraform.io/hashicorp/null in the future, but note that the + // current ProviderDownloaderClient implementation only supports the public + // standard registry (registry.terraform.io). + pAddr, err := tfaddr.ParseProviderSource(address) + if err != nil { + return nil, fmt.Errorf("failed to parse provider aaddress: %s", address) + } + + // Since .terraform.lock.hcl was introduced from v0.14, we assume that + // provider address is qualified with namespaces at least. We won't support + // implicit legacy things. + if !pAddr.HasKnownNamespace() { + return nil, fmt.Errorf("failed to parse unknown provider aaddress: %s", address) + } + if pAddr.IsLegacy() { + return nil, fmt.Errorf("failed to parse legacy provider aaddress: %s", address) + } + + pf := strings.Split(platform, "_") + if len(pf) != 2 { + return nil, fmt.Errorf("failed to parse platform: %s", platform) + } + os := pf[0] + arch := pf[1] + + req := &ProviderDownloadRequest{ + Namespace: pAddr.Namespace, + Type: pAddr.Type, + Version: version, + OS: os, + Arch: arch, + } + + return req, nil +} + +// buildProviderVersion calculates hash values from the ProviderDownloadResponse +// and returns an instance of the ProviderVersion. +func buildProviderVersion(address string, version string, platform string, res *ProviderDownloadResponse) (*ProviderVersion, error) { + h1Hashes := make(map[string]string) + + h1, err := zipDataToH1Hash(res.zipData) + if err != nil { + return nil, err + } + h1Hashes[res.filename] = h1 + + zhHashes, err := shaSumsDataToZhHash(res.shaSumsData) + if err != nil { + return nil, err + } + + pv := &ProviderVersion{ + address: address, + version: version, + platforms: []string{platform}, + h1Hashes: h1Hashes, + zhHashes: zhHashes, + } + + return pv, nil +} diff --git a/lock/index_test.go b/lock/index_test.go new file mode 100644 index 0000000..9eaf3af --- /dev/null +++ b/lock/index_test.go @@ -0,0 +1,314 @@ +package lock + +import ( + "context" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" +) + +// mockProviderDownloaderClient is a mock ProviderDownloaderAPI implementation. +type mockProviderDownloaderClient struct { + called int + responses []*ProviderDownloadResponse + errs []error +} + +var _ ProviderDownloaderAPI = (*mockProviderDownloaderClient)(nil) + +func (c *mockProviderDownloaderClient) ProviderDownload(ctx context.Context, req *ProviderDownloadRequest) (*ProviderDownloadResponse, error) { // nolint revive unused-parameter + res := c.responses[c.called] + err := c.errs[c.called] + c.called++ + return res, err +} + +func TestIndexGetOrCreateProviderVersion(t *testing.T) { + targetPlatforms := []string{"darwin_arm64"} + allPlatforms := []string{"darwin_arm64", "darwin_amd64", "linux_amd64", "windows_amd64"} + client := &mockProviderDownloaderClient{} + index := NewIndex(client) + + for _, address := range []string{"minamijoyo/dummy", "minamijoyo/null"} { + for _, version := range []string{"3.2.1", "3.2.2"} { + res, err := newMockProviderDownloadResponses(address, version, targetPlatforms, allPlatforms) + if err != nil { + t.Fatalf("failed to create mockResponses: err = %s", err) + } + // duplicate mocked responses + mockResponses := []*ProviderDownloadResponse{} + mockResponses = append(mockResponses, res...) + mockResponses = append(mockResponses, res...) + mockNoErrors := make([]error, len(targetPlatforms)*2) + // reuse the mocked client and set the mocked responses + client.responses = mockResponses + client.errs = mockNoErrors + client.called = 0 + + // 1st call + _, err = index.GetOrCreateProviderVersion(context.Background(), address, version, targetPlatforms) + if err != nil { + t.Fatalf("%s@%s: failed to call GetOrCreateProviderVersion: err = %s", address, version, err) + } + // expect cache miss + if client.called != len(targetPlatforms) { + t.Fatalf("%s@%s: api was called %d times, but expected to be called %d times", address, version, client.called, 1) + } + + // 2nd call + _, err = index.GetOrCreateProviderVersion(context.Background(), address, version, targetPlatforms) + if err != nil { + t.Fatalf("%s@%s: failed to call GetOrCreateProviderVersion: err = %s", address, version, err) + } + // expect cache hit + if client.called != len(targetPlatforms) { + t.Fatalf("%s@%s: api was called %d times, but expected to be called %d times", address, version, client.called, 1) + } + } + } +} + +func TestProviderIndexGetOrCreateProviderVersion(t *testing.T) { + allPlatforms := []string{"darwin_arm64", "darwin_amd64", "linux_amd64", "windows_amd64"} + + cases := []struct { + desc string + address string + version string + platforms []string + want *ProviderVersion + ok bool + }{ + { + desc: "simple", + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + want: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + ok: true, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + res, err := newMockProviderDownloadResponses(tc.address, tc.version, tc.platforms, allPlatforms) + if err != nil { + t.Fatalf("failed to create mockResponses: err = %s", err) + } + // duplicate mocked responses + mockResponses := []*ProviderDownloadResponse{} + mockResponses = append(mockResponses, res...) + mockResponses = append(mockResponses, res...) + mockNoErrors := make([]error, len(tc.platforms)*2) + client := &mockProviderDownloaderClient{ + responses: mockResponses, + errs: mockNoErrors, + } + pi := newProviderIndex(tc.address, client) + + // 1st call + got, err := pi.getOrCreateProviderVersion(context.Background(), tc.version, tc.platforms) + + if tc.ok && err != nil { + t.Fatalf("failed to call getOrCreateProviderVersion: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: got = %s", spew.Sdump(got)) + } + + if diff := cmp.Diff(got, tc.want, cmp.AllowUnexported(ProviderVersion{})); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.want), diff) + } + + // expect cache miss + if client.called != len(tc.platforms) { + t.Fatalf("api was called %d times, but expected to be called %d times", client.called, len(tc.platforms)) + } + + // 2nd call + cached, err := pi.getOrCreateProviderVersion(context.Background(), tc.version, tc.platforms) + + if tc.ok && err != nil { + t.Fatalf("failed to call getOrCreateProviderVersion: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: got = %s", spew.Sdump(cached)) + } + + if diff := cmp.Diff(cached, tc.want, cmp.AllowUnexported(ProviderVersion{})); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(cached), spew.Sdump(tc.want), diff) + } + + // expect cache hit + if client.called != len(tc.platforms) { + t.Fatalf("api was called %d times, but expected to be called %d times", client.called, len(tc.platforms)) + } + }) + } +} + +func TestNewProviderDownloadRequest(t *testing.T) { + cases := []struct { + desc string + address string + version string + platform string + want *ProviderDownloadRequest + ok bool + }{ + { + desc: "simple", + address: "minamijoyo/dummy", + version: "3.2.1", + platform: "darwin_arm64", + want: &ProviderDownloadRequest{ + Namespace: "minamijoyo", + Type: "dummy", + Version: "3.2.1", + OS: "darwin", + Arch: "arm64", + }, + ok: true, + }, + { + desc: "fully qualified provider address", + address: "registry.terraform.io/minamijoyo/dummy", + version: "3.2.1", + platform: "darwin_arm64", + want: &ProviderDownloadRequest{ + Namespace: "minamijoyo", + Type: "dummy", + Version: "3.2.1", + OS: "darwin", + Arch: "arm64", + }, + ok: true, + }, + { + desc: "unknown provider namespace", + address: "null", + version: "3.2.1", + platform: "darwin_arm64", + want: nil, + ok: false, + }, + { + desc: "legacy provider namespace", + address: "-/null", + version: "3.2.1", + platform: "darwin_arm64", + want: nil, + ok: false, + }, + { + desc: "zero provider namespace", + address: "", + version: "3.2.1", + platform: "darwin_arm64", + want: nil, + ok: false, + }, + { + desc: "invalid platform", + address: "minamijoyo/dummy", + version: "3.2.1", + platform: "foo", + want: nil, + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + got, err := newProviderDownloadRequest(tc.address, tc.version, tc.platform) + + if tc.ok && err != nil { + t.Fatalf("failed to call newProviderDownloadRequest: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: got = %s", spew.Sdump(got)) + } + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.want), diff) + } + }) + } +} + +func TestBuildProviderVersion(t *testing.T) { + allPlatforms := []string{"darwin_arm64", "darwin_amd64", "linux_amd64", "windows_amd64"} + + cases := []struct { + desc string + address string + version string + platform string + want *ProviderVersion + ok bool + }{ + { + desc: "simple", + address: "minamijoyo/dummy", + version: "3.2.1", + platform: "darwin_arm64", + want: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + ok: true, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + res, err := newMockProviderDownloadResponse(tc.address, tc.version, tc.platform, allPlatforms) + if err != nil { + t.Fatalf("failed to create mockResponse: err = %s", err) + } + + got, err := buildProviderVersion(tc.address, tc.version, tc.platform, res) + + if tc.ok && err != nil { + t.Fatalf("failed to call buildProviderVersion: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: got = %s", spew.Sdump(got)) + } + + if diff := cmp.Diff(got, tc.want, cmp.AllowUnexported(ProviderVersion{})); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.want), diff) + } + }) + } +} diff --git a/lock/mock.go b/lock/mock.go new file mode 100644 index 0000000..b53285f --- /dev/null +++ b/lock/mock.go @@ -0,0 +1,153 @@ +package lock + +import ( + "archive/zip" + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + + tfaddr "github.com/hashicorp/terraform-registry-address" + "golang.org/x/exp/slices" +) + +// newMockServer returns a new mock server for testing. +func newMockServer() (*http.ServeMux, *url.URL) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + mockServerURL, _ := url.Parse(server.URL) + return mux, mockServerURL +} + +// newTestClient returns a new client for testing. +func newTestClient(mockServerURL *url.URL, config TFRegistryConfig) *ProviderDownloaderClient { + config.BaseURL = mockServerURL.String() + c, _ := NewProviderDownloaderClient(config) + return c +} + +// newMockZipData returns a new zip format data for testing. +func newMockZipData(filename string, contents string) ([]byte, error) { + // create a zip file in memory + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + + // create a file in the zip file + w, err := zw.Create(filename) + if err != nil { + return nil, fmt.Errorf("failed to create a file in zip: err = %s", err) + } + _, err = w.Write([]byte(contents)) + if err != nil { + return nil, fmt.Errorf("failed to write contents to a file: err = %s", err) + } + + // zip + err = zw.Close() + if err != nil { + return nil, fmt.Errorf("failed to flush a zip file: err = %s", err) + } + + return buf.Bytes(), nil +} + +// newMockShaSumsData returns a new shaSumsData for testing. +// To ensure that the dummy data can be re-used in other test cases, the +// function really creates a zip file in memory and calculates its sha256sum. +func newMockShaSumsData(name string, version string, platforms []string) ([]byte, error) { + // terraform-provider-dummy_v3.2.1_x5 + filename := fmt.Sprintf("terraform-provider-%s_v%s_x5", name, version) + lines := []string{} + for _, platform := range platforms { + // dummy_3.2.1_darwin_arm64 + contents := fmt.Sprintf("%s_%s_%s", name, version, platform) + + // create a zip file in memory. + zipData, err := newMockZipData(filename, contents) + if err != nil { + return nil, fmt.Errorf("failed to create a zip file in memory: err = %s", err) + } + zh := sha256sumAsHexString(zipData) + zipFilename := "terraform-provider-" + contents + ".zip" + line := fmt.Sprintf("%s %s", zh, zipFilename) + lines = append(lines, line) + } + + slices.Sort(lines) + document := strings.Join(lines, "\n") + return []byte(document), nil +} + +// newMockProviderDownloadResponse returns a new ProviderDownloadResponse for testing. +func newMockProviderDownloadResponse(address string, version string, targetPlatform string, allPlatforms []string) (*ProviderDownloadResponse, error) { + pAddr, err := tfaddr.ParseProviderSource(address) + if err != nil { + return nil, fmt.Errorf("failed to parse provider aaddress: %s", address) + } + name := pAddr.Type + // create a zip file in memory. + zipDataFilename := fmt.Sprintf("terraform-provider-%s_v%s_x5", name, version) + zipDataContents := fmt.Sprintf("%s_%s_%s", name, version, targetPlatform) + zipData, err := newMockZipData(zipDataFilename, zipDataContents) + if err != nil { + return nil, fmt.Errorf("failed to create a zip file in memory: err = %s", err) + } + // create a valid dummy shaSumsData. + shaSumsData, err := newMockShaSumsData(name, version, allPlatforms) + if err != nil { + return nil, fmt.Errorf("failed to create a shaSumsData: err = %s", err) + } + filename := fmt.Sprintf("terraform-provider-%s_%s_%s.zip", name, version, targetPlatform) + return &ProviderDownloadResponse{ + filename: filename, + zipData: zipData, + shaSumsData: shaSumsData, + }, nil +} + +// newMockProviderDownloadResponses returns a new list of ProviderDownloadResponse for testing. +func newMockProviderDownloadResponses(address string, version string, targetPlatforms []string, allPlatforms []string) ([]*ProviderDownloadResponse, error) { + responses := []*ProviderDownloadResponse{} + for _, targetPlatform := range targetPlatforms { + res, err := newMockProviderDownloadResponse(address, version, targetPlatform, allPlatforms) + if err != nil { + return nil, err + } + responses = append(responses, res) + } + + return responses, nil +} + +// NewMockIndex does not call the real API but returns preset mock provider version metadata. +func NewMockIndex(pvs []*ProviderVersion) Index { + i := &index{ + providers: make(map[string]*providerIndex), + papi: nil, + } + for _, pv := range pvs { + pi, ok := i.providers[pv.address] + if !ok { + pi = newProviderIndex(pv.address, i.papi) + i.providers[pv.address] = pi + } + pi.versions[pv.version] = pv + } + + return i +} + +// NewMockProviderVersion returns a mocked ProviderVersion for testing. +// This is actually a setter to all private fields, but should not be used +// except for generating test data from outside the package. +func NewMockProviderVersion(address string, version string, platforms []string, h1Hashes map[string]string, zhHashes map[string]string) *ProviderVersion { + return &ProviderVersion{ + address: address, + version: version, + platforms: platforms, + h1Hashes: h1Hashes, + zhHashes: zhHashes, + } +} diff --git a/lock/provider_downloader.go b/lock/provider_downloader.go new file mode 100644 index 0000000..6d204e5 --- /dev/null +++ b/lock/provider_downloader.go @@ -0,0 +1,200 @@ +package lock + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "log" + "net/http" + "strings" + + "github.com/minamijoyo/tfupdate/tfregistry" +) + +// PackageDownloaderAPI is an interface for downloading provider package. +// Provider packages are downloaded from the HashiCorp release server, +// GitHub release page or somewhere else. +// Therefore we distinct this API from the Terraform Registry API. +// The API specification is not documented. +type ProviderDownloaderAPI interface { + // ProviderDownload downloads a provider package. + ProviderDownload(ctx context.Context, req *ProviderDownloadRequest) (*ProviderDownloadResponse, error) +} + +// ProviderDownloaderClient implements the ProviderDownloaderAPI interface +type ProviderDownloaderClient struct { + // api is an instance of TFRegistryAPI interface. + // It can be replaced for testing. + api TFRegistryAPI + + // httpClient is a http client which communicates with the ProviderDownloaderAPI. + httpClient *http.Client +} + +// ProviderDownloaderClient is a factory method which returns a ProviderDownloaderClient instance. +func NewProviderDownloaderClient(config TFRegistryConfig) (*ProviderDownloaderClient, error) { + // If config.api is not set, create a default TFRegistryClient + var api TFRegistryAPI + if config.api == nil { + var err error + api, err = NewTFRegistryClient(config) + if err != nil { + return nil, err + } + } else { + api = config.api + } + + return &ProviderDownloaderClient{ + api: api, + httpClient: &http.Client{}, + }, nil +} + +// ProviderDownloadRequest is a request type for ProviderDownload. +type ProviderDownloadRequest struct { + // (required): the namespace portion of the address of the requested provider. + Namespace string `json:"namespace"` + // (required): the type portion of the address of the requested provider. + Type string `json:"type"` + // (required): the version selected to download. + Version string `json:"version"` + // (required): a keyword identifying the operating system that the returned package should be compatible with, like "linux" or "darwin". + OS string `json:"os"` + // (required): a keyword identifying the CPU architecture that the returned package should be compatible with, like "amd64" or "arm". + Arch string `json:"arch"` +} + +// ProviderDownloadResponse is a response type for ProviderDownload. +type ProviderDownloadResponse struct { + // filename is the filename for zipData. + filename string + + // zipData is the raw byte sequence of the provider package. + zipData []byte + + // shaSumsData is the raw byte sequence of the provider shasum file. + shaSumsData []byte +} + +// ProviderDownload downloads a provider package. +func (c *ProviderDownloaderClient) ProviderDownload(ctx context.Context, req *ProviderDownloadRequest) (*ProviderDownloadResponse, error) { + metadataReq := &tfregistry.ProviderPackageMetadataRequest{ + Namespace: req.Namespace, + Type: req.Type, + Version: req.Version, + OS: req.OS, + Arch: req.Arch, + } + + metadataRes, err := c.api.ProviderPackageMetadata(ctx, metadataReq) + if err != nil { + return nil, err + } + + downloadURL := metadataRes.DownloadURL + zipData, err := c.download(ctx, downloadURL) + if err != nil { + return nil, err + } + + err = validateSHA256Sum(zipData, metadataRes.SHASum) + if err != nil { + return nil, err + } + + shaSumsURL := metadataRes.SHASumsURL + shaSumsData, err := c.download(ctx, shaSumsURL) + if err != nil { + return nil, err + } + + err = validateSHASumsData(shaSumsData, metadataRes.Filename, metadataRes.SHASum) + if err != nil { + return nil, err + } + + ret := &ProviderDownloadResponse{ + filename: metadataRes.Filename, + zipData: zipData, + shaSumsData: shaSumsData, + } + + return ret, nil +} + +// download is a helper function that downloads contents from a given url. +func (c *ProviderDownloaderClient) download(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to build http request: err = %s, url = %s", err, url) + } + + log.Printf("[DEBUG] ProviderDownloaderClient.download: GET %s", url) + res, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to request: err = %s, url = %s", err, url) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code %s: %s", res.Status, url) + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read body: err = %s, url = %s", err, url) + } + + return data, nil +} + +// validateSHA256Sum calculates the sha256 sum of the given byte sequence and +// checks whether it matches the expected hash value. +// The hash value is specified as a hexadecimal string. +func validateSHA256Sum(b []byte, sha256sum string) error { + got := sha256sumAsHexString(b) + if got != sha256sum { + return fmt.Errorf("checksum missmatch error. got = %s, expected = %s", got, sha256sum) + } + + return nil +} + +// sha256sumAsHexString calculates the sha256 sum of the given byte sequence and +// returns it as a hexadecimal string. +func sha256sumAsHexString(b []byte) string { + h := sha256.New() + h.Write(b) + return hex.EncodeToString(h.Sum(nil)) +} + +// validateSHASumsData checks whether the SHA256Sum document contains a matching hash value for a given filename. +func validateSHASumsData(b []byte, filename string, sha256sum string) error { + document := string(b) + for _, line := range strings.Split(document, "\n") { + // We expect that blank lines are not normally included, but to make the + // test data easier to read, ignore blank lines. + if len(line) == 0 { + continue + } + + // Split rows into columns with spaces, but note that there are two spaces between the columns. + // e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2 terraform-provider-null_3.2.1_darwin_arm64.zip + fields := strings.Fields(line) + if len(fields) != 2 { + return fmt.Errorf("checksum parse error: %s", document) + } + if fields[1] == filename { + if fields[0] != sha256sum { + return fmt.Errorf("checksum missmatch error. got = %s, expected = %s", fields[0], sha256sum) + } + return nil // ok + } + } + + // not found + return fmt.Errorf("checksum not found error: %s", document) +} diff --git a/lock/provider_downloader_test.go b/lock/provider_downloader_test.go new file mode 100644 index 0000000..761cc61 --- /dev/null +++ b/lock/provider_downloader_test.go @@ -0,0 +1,293 @@ +package lock + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/minamijoyo/tfupdate/tfregistry" +) + +func TestProviderDownloaderClientProviderDownload(t *testing.T) { + downloadPath := "/terraform-provider-dummy/3.2.1/terraform-provider-dummy_3.2.1_darwin_arm64.zip" + shaSumsPath := "/terraform-provider-dummy/3.2.1/terraform-provider-dummy_3.2.1_SHA256SUMS" + + // create a zip file in memory. + zipData, err := newMockZipData("terraform-provider-dummy_v3.2.1_x5", "dummy_3.2.1_darwin_arm64") + if err != nil { + t.Fatalf("failed to create a zip file in memory: err = %s", err) + } + + shaSumsData := []byte(` +5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086 terraform-provider-dummy_3.2.1_darwin_arm64.zip +8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e terraform-provider-dummy_3.2.1_windows_amd64.zip +c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2 terraform-provider-dummy_3.2.1_linux_amd64.zip +fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2 terraform-provider-dummy_3.2.1_darwin_amd64.zip +`) + + mux, mockServerURL := newMockServer() + mux.HandleFunc(downloadPath, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(zipData) + }) + mux.HandleFunc(shaSumsPath, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write(shaSumsData) + }) + + cases := []struct { + desc string + client *mockTFRegistryClient + want *ProviderDownloadResponse + ok bool + }{ + { + desc: "simple", + client: &mockTFRegistryClient{ + metadataRes: &tfregistry.ProviderPackageMetadataResponse{ + Filename: "terraform-provider-dummy_3.2.1_darwin_arm64.zip", + DownloadURL: mockServerURL.String() + downloadPath, + SHASum: sha256sumAsHexString(zipData), + SHASumsURL: mockServerURL.String() + shaSumsPath, + }, + err: nil, + }, + want: &ProviderDownloadResponse{ + filename: "terraform-provider-dummy_3.2.1_darwin_arm64.zip", + zipData: zipData, + shaSumsData: shaSumsData, + }, + ok: true, + }, + { + desc: "not found", + client: &mockTFRegistryClient{ + metadataRes: nil, + err: errors.New(`unexpected HTTP Status Code: 404`), + }, + want: nil, + ok: false, + }, + { + desc: "checksum missmatch", + client: &mockTFRegistryClient{ + metadataRes: &tfregistry.ProviderPackageMetadataResponse{ + Filename: "terraform-provider-dummy_3.2.1_darwin_arm64.zip", + DownloadURL: mockServerURL.String() + downloadPath, + SHASum: "aaa", + SHASumsURL: mockServerURL.String() + shaSumsPath, + }, + err: nil, + }, + want: nil, + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + config := TFRegistryConfig{ + api: tc.client, + } + client := newTestClient(mockServerURL, config) + + req := &ProviderDownloadRequest{ + Namespace: "minamijoyo", + Type: "dummy", + Version: "3.2.1", + OS: "darwin", + Arch: "arm64", + } + + got, err := client.ProviderDownload(context.Background(), req) + + if tc.ok && err != nil { + t.Fatalf("failed to call ProviderDownload: err = %s, req = %s", err, spew.Sdump(req)) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: req = %s, got = %s", spew.Sdump(req), spew.Sdump(got)) + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("got=%s, but want=%s", spew.Sdump(got), spew.Sdump(tc.want)) + } + }) + } +} + +func TestProviderDownloaderClientDownload(t *testing.T) { + subPath := "/terraform-provider-dummy/3.2.1/terraform-provider-dummy_3.2.1_darwin_arm64.zip" + cases := []struct { + desc string + subPath string + ok bool + code int + res []byte + want []byte + }{ + { + desc: "simple", + subPath: subPath, + ok: true, + code: 200, + // A byte sequence of zip should be returned, but for testing it does not + // have to be really a zip format, so we use a dummy string here for easy + // comparison in case of failure. + res: []byte("dummy"), + want: []byte("dummy"), + }, + { + desc: "not found", + subPath: subPath, + ok: false, + code: 404, + res: []byte(""), + want: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + mux, mockServerURL := newMockServer() + config := TFRegistryConfig{ + api: &mockTFRegistryClient{}, + } + client := newTestClient(mockServerURL, config) + mux.HandleFunc(tc.subPath, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tc.code) + _, _ = w.Write(tc.res) + }) + + mockServerURL.Path = subPath + reqURL := mockServerURL.String() + got, err := client.download(context.Background(), reqURL) + + if tc.ok && err != nil { + t.Fatalf("failed to call download: err = %s, req = %#v", err, tc.subPath) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: req = %#v, got = %#v", tc.subPath, got) + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("got=%#v, but want=%#v", got, tc.want) + } + }) + } +} + +func TestValidateSHA256Sum(t *testing.T) { + // create a zip file in memory. + zipData, err := newMockZipData("terraform-provider-dummy_v3.2.1_x5", "dummy_3.2.1_darwin_arm64") + if err != nil { + t.Fatalf("failed to create a zip file in memory: err = %s", err) + } + + cases := []struct { + desc string + b []byte + sha256sum string + ok bool + }{ + { + desc: "simple", + b: zipData, + sha256sum: "5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + ok: true, + }, + { + desc: "checksum missmatch", + b: zipData, + sha256sum: "aaa", + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + err := validateSHA256Sum(tc.b, tc.sha256sum) + + if tc.ok && err != nil { + t.Fatalf("failed to validate sha256sum: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatal("expected to fail, but success") + } + }) + } +} + +func TestValidateSHASumsData(t *testing.T) { + shaSumsData := []byte(` +5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086 terraform-provider-dummy_3.2.1_darwin_arm64.zip +8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e terraform-provider-dummy_3.2.1_windows_amd64.zip +c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2 terraform-provider-dummy_3.2.1_linux_amd64.zip +fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2 terraform-provider-dummy_3.2.1_darwin_amd64.zip +`) + + cases := []struct { + desc string + b []byte + filename string + sha256sum string + ok bool + }{ + { + desc: "simple", + b: shaSumsData, + filename: "terraform-provider-dummy_3.2.1_darwin_arm64.zip", + sha256sum: "5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + ok: true, + }, + { + desc: "checksum missmatch", + b: shaSumsData, + filename: "terraform-provider-dummy_3.2.1_darwin_arm64.zip", + sha256sum: "aaa", + ok: false, + }, + { + desc: "not found", + b: shaSumsData, + filename: "foo.zip", + sha256sum: "5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + ok: false, + }, + { + desc: "parse error", + b: []byte(` +5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086 +`), + filename: "terraform-provider-dummy_3.2.1_darwin_arm64.zip", + sha256sum: "5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + ok: false, + }, + { + desc: "empty", + b: []byte(""), + filename: "terraform-provider-dummy_3.2.1_darwin_arm64.zip", + sha256sum: "5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + err := validateSHASumsData(tc.b, tc.filename, tc.sha256sum) + + if tc.ok && err != nil { + t.Fatalf("failed to validate SHASumsData: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatal("expected to fail, but success") + } + }) + } +} diff --git a/lock/provider_version.go b/lock/provider_version.go new file mode 100644 index 0000000..7f56069 --- /dev/null +++ b/lock/provider_version.go @@ -0,0 +1,84 @@ +package lock + +import ( + "fmt" + "reflect" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// ProviderVersion is a data structure that holds hash values of a specific +// version of a particular provider. It corresponds to one provider block in +// the dependency lock file (.terraform.lock.hcl). +// https://developer.hashicorp.com/terraform/language/files/dependency-lock +type ProviderVersion struct { + // address is a provider address such as hashicorp/null. + address string + + // version is a version number such as 3.2.1. + version string + + // platforms is a list of target platforms to generate hash values. + // Target platform names consist of an operating system and a CPU architecture such as darwin_arm64. + // The actual lock file does not distinguish which platform the hash values + // belong to, but we keep them distinct in memory for easy debugging in case + // of checksum mismatches. + platforms []string + + // h1Hashes is a dictionary of hash values calculated with the h1 scheme. + // The key is the filename. + h1Hashes map[string]string + + // zhHashes is a dictionary of hash values calculated with the zh scheme. + // The key is the filename. + zhHashes map[string]string +} + +// newEmptyProviderVersion returns a new empty ProviderVersion, which is +// intended to be used as a variable to store merge results. +func newEmptyProviderVersion(address string, version string) *ProviderVersion { + return &ProviderVersion{ + address: address, + version: version, + platforms: make([]string, 0), + h1Hashes: make(map[string]string, 0), + zhHashes: make(map[string]string, 0), + } +} + +// Merge takes another ProviderVersion and merges it. It returns an error if +// the argument is incompatible the current object. +func (pv *ProviderVersion) Merge(rhs *ProviderVersion) error { + if pv.address != rhs.address { + return fmt.Errorf("failed to merge ProviderVersion.address: %s != %s", pv.address, rhs.address) + } + if pv.version != rhs.version { + return fmt.Errorf("failed to merge ProviderVersion.version: %s != %s", pv.version, rhs.version) + } + + pv.platforms = append(pv.platforms, rhs.platforms...) + maps.Copy(pv.h1Hashes, rhs.h1Hashes) + + if len(pv.zhHashes) != 0 { + if !reflect.DeepEqual(pv.zhHashes, rhs.zhHashes) { + // should not happen + return fmt.Errorf("failed to merge ProviderVersion.zhHashes: %#v != %#v", pv.zhHashes, rhs.zhHashes) + } + } else { + pv.zhHashes = rhs.zhHashes + } + + return nil +} + +// AllHashes returns an array of strings containing all hash values. It is +// intended to be used as the value of hashes in a dependency lock file. +// The result is sorted alphabetically. +func (pv *ProviderVersion) AllHashes() []string { + h1 := maps.Values(pv.h1Hashes) + zh := maps.Values(pv.zhHashes) + hashes := append(h1, zh...) + slices.Sort(hashes) + return hashes +} diff --git a/lock/provider_version_test.go b/lock/provider_version_test.go new file mode 100644 index 0000000..b7a9031 --- /dev/null +++ b/lock/provider_version_test.go @@ -0,0 +1,231 @@ +package lock + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" +) + +func TestProviderVersionMerge(t *testing.T) { + cases := []struct { + desc string + pv *ProviderVersion + rhs *ProviderVersion + want *ProviderVersion + ok bool + }{ + { + desc: "simple", + pv: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64", "darwin_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + rhs: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"linux_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + want: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + ok: true, + }, + { + desc: "merge to empty", + pv: newEmptyProviderVersion("minamijoyo/dummy", "3.2.1"), + rhs: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"linux_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + want: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"linux_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + ok: true, + }, + { + desc: "address mismatch", + pv: newEmptyProviderVersion("minamijoyo/dummy", "3.2.1"), + rhs: newEmptyProviderVersion("minamijoyo/foo", "3.2.1"), + want: newEmptyProviderVersion("minamijoyo/dummy", "3.2.1"), + ok: false, + }, + { + desc: "version mismatch", + pv: newEmptyProviderVersion("minamijoyo/dummy", "3.2.1"), + rhs: newEmptyProviderVersion("minamijoyo/dummy", "3.2.0"), + want: newEmptyProviderVersion("minamijoyo/dummy", "3.2.1"), + ok: false, + }, + { + desc: "zh mismatch", + pv: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64", "darwin_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + rhs: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"linux_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + want: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + + err := tc.pv.Merge(tc.rhs) + + if tc.ok && err != nil { + t.Fatalf("failed to call Merge: err = %s", err) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: got = %s", spew.Sdump(tc.pv)) + } + + if diff := cmp.Diff(tc.pv, tc.want, cmp.AllowUnexported(ProviderVersion{})); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(tc.pv), spew.Sdump(tc.want), diff) + } + }) + } +} + +func TestProviderVersionAllHashes(t *testing.T) { + cases := []struct { + desc string + pv *ProviderVersion + want []string + }{ + { + desc: "simple", + pv: &ProviderVersion{ + address: "minamijoyo/dummy", + version: "3.2.1", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64", "windows_amd64"}, + h1Hashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + }, + zhHashes: map[string]string{ + "terraform-provider-dummy_3.2.1_darwin_arm64.zip": "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "terraform-provider-dummy_3.2.1_darwin_amd64.zip": "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + "terraform-provider-dummy_3.2.1_linux_amd64.zip": "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "terraform-provider-dummy_3.2.1_windows_amd64.zip": "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + }, + }, + want: []string{ + "h1:2zotrPRAjGZZMkjJGBGLnIbG+sqhQN30sbwqSDECQFQ=", + "h1:3323G20HW9PA9ONrL6CdQCdCFe6y94kXeOTprq+Zu+w=", + "h1:63My0EuWIYHWVwWOxmxWwgrfx+58Tz+nTduelaCCAfs=", + "zh:5622a0fd03420ed1fa83a1a6e90b65fbe34bc74c251b3b47048f14217e93b086", + "zh:8b75ff41191a7fe6c5d9129ed19a01eacde5a3797b48b738eefa21f5330c081e", + "zh:c5f0a44e3a3795cb3ee0abb0076097c738294c241f74c145dfb50f2b9fd71fd2", + "zh:fc5bbdd0a1bd6715b9afddf3aba6acc494425d77015c19579b9a9fa950e532b2", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + + got := tc.pv.AllHashes() + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.want), diff) + } + }) + } +} diff --git a/lock/tfregistry.go b/lock/tfregistry.go new file mode 100644 index 0000000..7d9dd0a --- /dev/null +++ b/lock/tfregistry.go @@ -0,0 +1,61 @@ +package lock + +import ( + "context" + "fmt" + "net/url" + + "github.com/minamijoyo/tfupdate/tfregistry" +) + +// TFRegistryAPI is an interface which calls Terraform Registry API. +// This abstraction layer is needed for testing with mock. +type TFRegistryAPI interface { + // ProviderPackageMetadata returns a package metadata of a provider. + // https://developer.hashicorp.com/terraform/internals/provider-registry-protocol#find-a-provider-package + ProviderPackageMetadata(ctx context.Context, req *tfregistry.ProviderPackageMetadataRequest) (*tfregistry.ProviderPackageMetadataResponse, error) +} + +// TFRegistryConfig is a set of configurations for ProviderDownloaderClient. +type TFRegistryConfig struct { + // api is an instance of TFRegistryAPI interface. + // It can be replaced for testing. + api TFRegistryAPI + + // BaseURL is a URL for Terraform Registry API requests. + // Defaults to the public Terraform Registry API. + // This looks like the Terraform Cloud support, but currently for testing purposes only. + // The Terraform Cloud is not supported yet. + // BaseURL should always be specified with a trailing slash. + BaseURL string +} + +// TFRegistryClient is a real TFRegistryAPI implementation. +type TFRegistryClient struct { + client *tfregistry.Client +} + +var _ TFRegistryAPI = (*TFRegistryClient)(nil) + +// NewTFRegistryClient returns a real TFRegistryClient instance. +func NewTFRegistryClient(config TFRegistryConfig) (*TFRegistryClient, error) { + c := tfregistry.NewClient(nil) + + if len(config.BaseURL) != 0 { + baseURL, err := url.Parse(config.BaseURL) + if err != nil { + return nil, fmt.Errorf("failed to parse tfregistry base url: %s", err) + } + c.BaseURL = baseURL + } + + return &TFRegistryClient{ + client: c, + }, nil +} + +// ProviderPackageMetadata returns a package metadata of a provider. +// https://developer.hashicorp.com/terraform/internals/provider-registry-protocol#find-a-provider-package +func (c *TFRegistryClient) ProviderPackageMetadata(ctx context.Context, req *tfregistry.ProviderPackageMetadataRequest) (*tfregistry.ProviderPackageMetadataResponse, error) { + return c.client.ProviderPackageMetadata(ctx, req) +} diff --git a/lock/tfregistry_test.go b/lock/tfregistry_test.go new file mode 100644 index 0000000..db99219 --- /dev/null +++ b/lock/tfregistry_test.go @@ -0,0 +1,70 @@ +package lock + +import ( + "context" + "testing" + + "github.com/minamijoyo/tfupdate/tfregistry" +) + +// mockTFRegistryClient is a mock TFRegistryAPI implementation. +type mockTFRegistryClient struct { + metadataRes *tfregistry.ProviderPackageMetadataResponse + err error +} + +var _ TFRegistryAPI = (*mockTFRegistryClient)(nil) + +func (c *mockTFRegistryClient) ProviderPackageMetadata(ctx context.Context, req *tfregistry.ProviderPackageMetadataRequest) (*tfregistry.ProviderPackageMetadataResponse, error) { // nolint revive unused-parameter + return c.metadataRes, c.err +} + +func TestNewTFRegistryClient(t *testing.T) { + cases := []struct { + baseURL string + want string + ok bool + }{ + { + baseURL: "", + want: "https://registry.terraform.io/", + ok: true, + }, + { + baseURL: "https://registry.terraform.io/", + want: "https://registry.terraform.io/", + ok: true, + }, + { + baseURL: "http://localhost/", + want: "http://localhost/", + ok: true, + }, + { + baseURL: `https://registry\.terraform.io/`, + want: "", + ok: false, + }, + } + + for _, tc := range cases { + config := TFRegistryConfig{ + BaseURL: tc.baseURL, + } + got, err := NewTFRegistryClient(config) + + if tc.ok && err != nil { + t.Errorf("NewTFRegistryClient() with baseURL = %s returns unexpected err: %s", tc.baseURL, err) + } + + if !tc.ok && err == nil { + t.Errorf("NewTFRegistryClient() with baseURL = %s expects to return an error, but no error", tc.baseURL) + } + + if tc.ok { + if got.client.BaseURL.String() != tc.want { + t.Errorf("NewTFRegistryClient() with baseURL = %s returns %s, but want %s", tc.baseURL, got.client.BaseURL.String(), tc.want) + } + } + } +} diff --git a/main.go b/main.go index 1df4aeb..8fea505 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,11 @@ func initCommands() map[string]cli.CommandFactory { Meta: meta, }, nil }, + "lock": func() (cli.Command, error) { + return &command.LockCommand{ + Meta: meta, + }, nil + }, "release": func() (cli.Command, error) { return &command.ReleaseCommand{ Meta: meta, diff --git a/scripts/testacc/all.sh b/scripts/testacc/all.sh new file mode 100755 index 0000000..c80f91a --- /dev/null +++ b/scripts/testacc/all.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -eo pipefail + +script_full_path=$(dirname "$0") + +# test simple +bash "$script_full_path/lock.sh" run simple + +# test all +repo_root_dir="$(git rev-parse --show-toplevel)" +fixturesdir="$repo_root_dir/test-fixtures/lock/" + +fixtures=$(find "$fixturesdir" -type d -mindepth 1 -maxdepth 1 -exec basename {} \; | sort) + +for fixture in ${fixtures} +do + echo "$fixture" + bash "$script_full_path/lock.sh" run "$fixture" +done diff --git a/scripts/testacc/lock.sh b/scripts/testacc/lock.sh new file mode 100755 index 0000000..a291b3d --- /dev/null +++ b/scripts/testacc/lock.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +set -eo pipefail + +usage() +{ + cat << EOF + Usage: $(basename "$0") + + Arguments: + command: A name of step to run. Valid values are: + run | setup | provider | lock | cleanup + fixture: A name of fixture in test-fixtures/lock/ +EOF +} + +setup() +{ + cp -prT "$FIXTUREDIR" ./ + ALL_DIRS=$(find . -type f -print0 -name '*.tf' | xargs -0 -I {} dirname {} | sort | uniq | grep -v 'modules/') + for dir in ${ALL_DIRS} + do + pushd "$dir" + + # always create a new lock + rm -f .terraform.lock.hcl + terraform providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64 + cat .terraform.lock.hcl + rm -rf .terraform + + popd + done +} + +provider() +{ + TFUPDATE_LOG=DEBUG tfupdate provider null -v 3.2.1 -r ./ +} + +lock() +{ + TFUPDATE_LOG=DEBUG tfupdate lock --platform=linux_amd64 --platform=darwin_amd64 --platform=darwin_arm64 -r ./ + + ALL_DIRS=$(find . -type f -print0 -name '*.tf' | xargs -0 -I {} dirname {} | sort | uniq | grep -v 'modules/') + for dir in ${ALL_DIRS} + do + pushd "$dir" + + # got + mv .terraform.lock.hcl .terraform.lock.hcl.got + + # want + terraform providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64 + + # assert the result + cat .terraform.lock.hcl + cat .terraform.lock.hcl.got + diff -u .terraform.lock.hcl .terraform.lock.hcl.got + + popd + done +} + +cleanup() +{ + find ./ -mindepth 1 -delete +} + +run() +{ + setup + provider + lock + cleanup +} + +# main +if [[ $# -ne 2 ]]; then + usage + exit 1 +fi + +set -x + +COMMAND=$1 +FIXTURE=$2 + +REPO_ROOT_DIR="$(git rev-parse --show-toplevel)" +WORKDIR="$REPO_ROOT_DIR/tmp/testacc/lock/$FIXTURE" +FIXTUREDIR="$REPO_ROOT_DIR/test-fixtures/lock/$FIXTURE/" +mkdir -p "$WORKDIR" +pushd "$WORKDIR" + +case "$COMMAND" in + run | setup | provider | lock | cleanup ) + "$COMMAND" + RET=$? + ;; + *) + usage + RET=1 + ;; +esac + +popd +exit $RET diff --git a/test-fixtures/lock/simple/dir1/main.tf b/test-fixtures/lock/simple/dir1/main.tf new file mode 100644 index 0000000..77f8eec --- /dev/null +++ b/test-fixtures/lock/simple/dir1/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.1.1" + } + } +} diff --git a/test-fixtures/lock/simple/dir2/main.tf b/test-fixtures/lock/simple/dir2/main.tf new file mode 100644 index 0000000..77f8eec --- /dev/null +++ b/test-fixtures/lock/simple/dir2/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.1.1" + } + } +} diff --git a/test-fixtures/lock/simple/main.tf b/test-fixtures/lock/simple/main.tf new file mode 100644 index 0000000..77f8eec --- /dev/null +++ b/test-fixtures/lock/simple/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.1.1" + } + } +} diff --git a/tfregistry/module.go b/tfregistry/module.go index 5d8a101..5c713b4 100644 --- a/tfregistry/module.go +++ b/tfregistry/module.go @@ -2,7 +2,6 @@ package tfregistry import ( "context" - "fmt" ) const ( @@ -22,60 +21,3 @@ type ModuleV1API interface { // https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider ModuleLatestForProvider(ctx context.Context, req *ModuleLatestForProviderRequest) (*ModuleLatestForProviderResponse, error) } - -// ModuleLatestForProviderRequest is a request parameter for ModuleLatestForProvider(). -// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider -type ModuleLatestForProviderRequest struct { - // Namespace is a user name which owns the module. - Namespace string `json:"namespace"` - // Name is a name of the module. - Name string `json:"name"` - // Provider is a name of the provider. - Provider string `json:"provider"` -} - -// ModuleLatestForProviderResponse is a response data for ModuleLatestForProvider(). -// There are other response fields, but we define only those we need here. -type ModuleLatestForProviderResponse struct { - // Version is the latest version of the module for a specific provider. - Version string `json:"version"` - // Versions is a list of available versions. - Versions []string `json:"versions"` -} - -// ModuleLatestForProvider returns the latest version of a module for a single provider. -// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider -func (c *Client) ModuleLatestForProvider(ctx context.Context, req *ModuleLatestForProviderRequest) (*ModuleLatestForProviderResponse, error) { - if len(req.Namespace) == 0 { - return nil, fmt.Errorf("Invalid request. Namespace is required. req = %#v", req) - } - if len(req.Name) == 0 { - return nil, fmt.Errorf("Invalid request. Name is required. req = %#v", req) - } - if len(req.Provider) == 0 { - return nil, fmt.Errorf("Invalid request. Provider is required. req = %#v", req) - } - - subPath := fmt.Sprintf("%s%s/%s/%s", moduleV1Service, req.Namespace, req.Name, req.Provider) - - httpRequest, err := c.newRequest(ctx, "GET", subPath, nil) - if err != nil { - return nil, err - } - - httpResponse, err := c.httpClient.Do(httpRequest) - if err != nil { - return nil, fmt.Errorf("failed to HTTP Request: err = %s, req = %#v", err, httpRequest) - } - - if httpResponse.StatusCode != 200 { - return nil, fmt.Errorf("unexpected HTTP Status Code: %d", httpResponse.StatusCode) - } - - var res ModuleLatestForProviderResponse - if err := decodeBody(httpResponse, &res); err != nil { - return nil, err - } - - return &res, nil -} diff --git a/tfregistry/module_latest.go b/tfregistry/module_latest.go new file mode 100644 index 0000000..86bf29c --- /dev/null +++ b/tfregistry/module_latest.go @@ -0,0 +1,63 @@ +package tfregistry + +import ( + "context" + "fmt" +) + +// ModuleLatestForProviderRequest is a request parameter for ModuleLatestForProvider(). +// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider +type ModuleLatestForProviderRequest struct { + // Namespace is a user name which owns the module. + Namespace string `json:"namespace"` + // Name is a name of the module. + Name string `json:"name"` + // Provider is a name of the provider. + Provider string `json:"provider"` +} + +// ModuleLatestForProviderResponse is a response data for ModuleLatestForProvider(). +// There are other response fields, but we define only those we need here. +type ModuleLatestForProviderResponse struct { + // Version is the latest version of the module for a specific provider. + Version string `json:"version"` + // Versions is a list of available versions. + Versions []string `json:"versions"` +} + +// ModuleLatestForProvider returns the latest version of a module for a single provider. +// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider +func (c *Client) ModuleLatestForProvider(ctx context.Context, req *ModuleLatestForProviderRequest) (*ModuleLatestForProviderResponse, error) { + if len(req.Namespace) == 0 { + return nil, fmt.Errorf("Invalid request. Namespace is required. req = %#v", req) + } + if len(req.Name) == 0 { + return nil, fmt.Errorf("Invalid request. Name is required. req = %#v", req) + } + if len(req.Provider) == 0 { + return nil, fmt.Errorf("Invalid request. Provider is required. req = %#v", req) + } + + subPath := fmt.Sprintf("%s%s/%s/%s", moduleV1Service, req.Namespace, req.Name, req.Provider) + + httpRequest, err := c.newRequest(ctx, "GET", subPath, nil) + if err != nil { + return nil, err + } + + httpResponse, err := c.httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("failed to HTTP Request: err = %s, req = %#v", err, httpRequest) + } + + if httpResponse.StatusCode != 200 { + return nil, fmt.Errorf("unexpected HTTP Status Code: %d", httpResponse.StatusCode) + } + + var res ModuleLatestForProviderResponse + if err := decodeBody(httpResponse, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/tfregistry/module_test.go b/tfregistry/module_latest_test.go similarity index 100% rename from tfregistry/module_test.go rename to tfregistry/module_latest_test.go diff --git a/tfregistry/provider.go b/tfregistry/provider.go index 60abb73..9cb8b66 100644 --- a/tfregistry/provider.go +++ b/tfregistry/provider.go @@ -2,7 +2,6 @@ package tfregistry import ( "context" - "fmt" ) const ( @@ -22,58 +21,8 @@ type ProviderV1API interface { // This relies on a currently undocumented providers API endpoint which behaves exactly like the equivalent documented modules API endpoint. // https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider ProviderLatest(ctx context.Context, req *ProviderLatestRequest) (*ProviderLatestResponse, error) -} - -// ProviderLatestRequest is a request parameter for ProviderLatest(). -// This relies on a currently undocumented providers API endpoint which behaves exactly like the equivalent documented modules API endpoint. -// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider -type ProviderLatestRequest struct { - // Namespace is the name of a namespace, unique on a particular hostname, that can contain one or more providers that are somehow related. On the public Terraform Registry the "namespace" represents the organization that is packaging and distributing the provider. - Namespace string `json:"namespace"` - // Type is the provider type, like "azurerm", "aws", "google", "dns", etc. A provider type is unique within a particular hostname and namespace. - Type string `json:"type"` -} - -// ProviderLatestResponse is a response data for ProviderLatest(). -// There are other response fields, but we define only those we need here. -type ProviderLatestResponse struct { - // Version is the latest version of the provider. - Version string `json:"version"` - // Versions is a list of available versions. - Versions []string `json:"versions"` -} - -// ProviderLatest returns the latest version of a provider. -// This relies on a currently undocumented providers API endpoint which behaves exactly like the equivalent documented modules API endpoint. -// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider -func (c *Client) ProviderLatest(ctx context.Context, req *ProviderLatestRequest) (*ProviderLatestResponse, error) { - if len(req.Namespace) == 0 { - return nil, fmt.Errorf("Invalid request. Namespace is required. req = %#v", req) - } - if len(req.Type) == 0 { - return nil, fmt.Errorf("Invalid request. Type is required. req = %#v", req) - } - - subPath := fmt.Sprintf("%s%s/%s", providerV1Service, req.Namespace, req.Type) - - httpRequest, err := c.newRequest(ctx, "GET", subPath, nil) - if err != nil { - return nil, err - } - - httpResponse, err := c.httpClient.Do(httpRequest) - if err != nil { - return nil, fmt.Errorf("failed to HTTP Request: err = %s, req = %#v", err, httpRequest) - } - - if httpResponse.StatusCode != 200 { - return nil, fmt.Errorf("unexpected HTTP Status Code: %d", httpResponse.StatusCode) - } - - var res ProviderLatestResponse - if err := decodeBody(httpResponse, &res); err != nil { - return nil, err - } - return &res, nil + // ProviderPackageMetadata returns a package metadata of a provider. + // https://developer.hashicorp.com/terraform/internals/provider-registry-protocol#find-a-provider-package + ProviderPackageMetadata(ctx context.Context, req *ProviderPackageMetadataRequest) (*ProviderPackageMetadataResponse, error) } diff --git a/tfregistry/provider_latest.go b/tfregistry/provider_latest.go new file mode 100644 index 0000000..da2b70e --- /dev/null +++ b/tfregistry/provider_latest.go @@ -0,0 +1,60 @@ +package tfregistry + +import ( + "context" + "fmt" +) + +// ProviderLatestRequest is a request parameter for ProviderLatest(). +// This relies on a currently undocumented providers API endpoint which behaves exactly like the equivalent documented modules API endpoint. +// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider +type ProviderLatestRequest struct { + // Namespace is the name of a namespace, unique on a particular hostname, that can contain one or more providers that are somehow related. On the public Terraform Registry the "namespace" represents the organization that is packaging and distributing the provider. + Namespace string `json:"namespace"` + // Type is the provider type, like "azurerm", "aws", "google", "dns", etc. A provider type is unique within a particular hostname and namespace. + Type string `json:"type"` +} + +// ProviderLatestResponse is a response data for ProviderLatest(). +// There are other response fields, but we define only those we need here. +type ProviderLatestResponse struct { + // Version is the latest version of the provider. + Version string `json:"version"` + // Versions is a list of available versions. + Versions []string `json:"versions"` +} + +// ProviderLatest returns the latest version of a provider. +// This relies on a currently undocumented providers API endpoint which behaves exactly like the equivalent documented modules API endpoint. +// https://www.terraform.io/docs/registry/api.html#latest-version-for-a-specific-module-provider +func (c *Client) ProviderLatest(ctx context.Context, req *ProviderLatestRequest) (*ProviderLatestResponse, error) { + if len(req.Namespace) == 0 { + return nil, fmt.Errorf("Invalid request. Namespace is required. req = %#v", req) + } + if len(req.Type) == 0 { + return nil, fmt.Errorf("Invalid request. Type is required. req = %#v", req) + } + + subPath := fmt.Sprintf("%s%s/%s", providerV1Service, req.Namespace, req.Type) + + httpRequest, err := c.newRequest(ctx, "GET", subPath, nil) + if err != nil { + return nil, err + } + + httpResponse, err := c.httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("failed to HTTP Request: err = %s, req = %#v", err, httpRequest) + } + + if httpResponse.StatusCode != 200 { + return nil, fmt.Errorf("unexpected HTTP Status Code: %d", httpResponse.StatusCode) + } + + var res ProviderLatestResponse + if err := decodeBody(httpResponse, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/tfregistry/provider_test.go b/tfregistry/provider_latest_test.go similarity index 100% rename from tfregistry/provider_test.go rename to tfregistry/provider_latest_test.go diff --git a/tfregistry/provider_package_metadata.go b/tfregistry/provider_package_metadata.go new file mode 100644 index 0000000..095bf32 --- /dev/null +++ b/tfregistry/provider_package_metadata.go @@ -0,0 +1,79 @@ +package tfregistry + +import ( + "context" + "fmt" + "log" +) + +// ProviderPackageMetadataRequest is a request parameter for ProviderPackageMetadata(). +// https://developer.hashicorp.com/terraform/internals/provider-registry-protocol#find-a-provider-package +type ProviderPackageMetadataRequest struct { + // (required): the namespace portion of the address of the requested provider. + Namespace string `json:"namespace"` + // (required): the type portion of the address of the requested provider. + Type string `json:"type"` + // (required): the version selected to download. + Version string `json:"version"` + // (required): a keyword identifying the operating system that the returned package should be compatible with, like "linux" or "darwin". + OS string `json:"os"` + // (required): a keyword identifying the CPU architecture that the returned package should be compatible with, like "amd64" or "arm". + Arch string `json:"arch"` +} + +// ProviderPackageMetadataResponse is a response data for ProviderPackageMetadata(). +// There are other response fields, but we define only those we need here. +type ProviderPackageMetadataResponse struct { + // (required): the filename for this provider's zip archive as recorded in the "shasums" document, so that Terraform CLI can determine which of the given checksums should be used for this specific package. + Filename string `json:"filename"` + // (required): a URL from which Terraform can retrieve the provider's zip archive. If this is a relative URL then it will be resolved relative to the URL that returned the containing JSON object. + DownloadURL string `json:"download_url"` + // (required): the SHA256 checksum for this provider's zip archive as recorded in the shasums document. + SHASum string `json:"shasum"` + // (required): a URL from which Terraform can retrieve a text document recording expected SHA256 checksums for this package and possibly other packages for the same provider version on other platforms. + SHASumsURL string `json:"shasums_url"` +} + +// ProviderPackageMetadata returns a package metadata of a provider. +// https://developer.hashicorp.com/terraform/internals/provider-registry-protocol#find-a-provider-package +func (c *Client) ProviderPackageMetadata(ctx context.Context, req *ProviderPackageMetadataRequest) (*ProviderPackageMetadataResponse, error) { + if len(req.Namespace) == 0 { + return nil, fmt.Errorf("Invalid request. Namespace is required. req = %#v", req) + } + if len(req.Type) == 0 { + return nil, fmt.Errorf("Invalid request. Type is required. req = %#v", req) + } + if len(req.Version) == 0 { + return nil, fmt.Errorf("Invalid request. Version is required. req = %#v", req) + } + if len(req.OS) == 0 { + return nil, fmt.Errorf("Invalid request. OS is required. req = %#v", req) + } + if len(req.Arch) == 0 { + return nil, fmt.Errorf("Invalid request. Arch is required. req = %#v", req) + } + + subPath := fmt.Sprintf("%s%s/%s/%s/download/%s/%s", providerV1Service, req.Namespace, req.Type, req.Version, req.OS, req.Arch) + + httpRequest, err := c.newRequest(ctx, "GET", subPath, nil) + if err != nil { + return nil, err + } + + log.Printf("[DEBUG] Client.ProviderPackageMetadata: GET %s", httpRequest.URL) + httpResponse, err := c.httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("failed to HTTP Request: err = %s, req = %#v", err, httpRequest) + } + + if httpResponse.StatusCode != 200 { + return nil, fmt.Errorf("unexpected HTTP Status Code: %d", httpResponse.StatusCode) + } + + var res ProviderPackageMetadataResponse + if err := decodeBody(httpResponse, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/tfregistry/provider_package_metadata_test.go b/tfregistry/provider_package_metadata_test.go new file mode 100644 index 0000000..cd263c4 --- /dev/null +++ b/tfregistry/provider_package_metadata_test.go @@ -0,0 +1,175 @@ +package tfregistry + +import ( + "context" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestProviderPackageMetadata(t *testing.T) { + cases := []struct { + desc string + req *ProviderPackageMetadataRequest + ok bool + code int + res string + want *ProviderPackageMetadataResponse + }{ + { + desc: "simple", + req: &ProviderPackageMetadataRequest{ + Namespace: "hashicorp", + Type: "null", + Version: "3.2.1", + OS: "darwin", + Arch: "arm64", + }, + ok: true, + code: 200, + res: mockProviderPackageMetadataResponse, + want: &ProviderPackageMetadataResponse{ + Filename: "terraform-provider-null_3.2.1_darwin_arm64.zip", + DownloadURL: "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_darwin_arm64.zip", + SHASum: "e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + SHASumsURL: "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_SHA256SUMS", + }, + }, + { + desc: "not found", + req: &ProviderPackageMetadataRequest{ + Namespace: "hoge", + Type: "piyo", + Version: "3.2.1", + OS: "darwin", + Arch: "arm64", + }, + ok: false, + code: 404, + res: `{"errors":["Not Found"]}`, + want: nil, + }, + { + desc: "invalid request (Namespace)", + req: &ProviderPackageMetadataRequest{ + Namespace: "", + Type: "piyo", + Version: "3.2.1", + OS: "darwin", + Arch: "arm64", + }, + ok: false, + code: 0, + res: "", + want: nil, + }, + { + desc: "invalid request (Type)", + req: &ProviderPackageMetadataRequest{ + Namespace: "hoge", + Type: "", + Version: "3.2.1", + OS: "darwin", + Arch: "arm64", + }, + ok: false, + code: 0, + res: "", + want: nil, + }, + { + desc: "invalid request (Version)", + req: &ProviderPackageMetadataRequest{ + Namespace: "hoge", + Type: "piyo", + Version: "", + OS: "darwin", + Arch: "arm64", + }, + ok: false, + code: 0, + res: "", + want: nil, + }, + { + desc: "invalid request (OS)", + req: &ProviderPackageMetadataRequest{ + Namespace: "hoge", + Type: "piyo", + Version: "3.2.1", + OS: "", + Arch: "arm64", + }, + ok: false, + code: 0, + res: "", + want: nil, + }, + { + desc: "invalid request (Arch)", + req: &ProviderPackageMetadataRequest{ + Namespace: "hoge", + Type: "piyo", + Version: "3.2.1", + OS: "darwin", + Arch: "", + }, + ok: false, + code: 0, + res: "", + want: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + mux, mockServerURL := newMockServer() + client := newTestClient(mockServerURL) + subPath := fmt.Sprintf("%s%s/%s/%s/download/%s/%s", providerV1Service, tc.req.Namespace, tc.req.Type, tc.req.Version, tc.req.OS, tc.req.Arch) + mux.HandleFunc(subPath, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tc.code) + fmt.Fprint(w, tc.res) + }) + + got, err := client.ProviderPackageMetadata(context.Background(), tc.req) + + if tc.ok && err != nil { + t.Fatalf("failed to call ProviderPackageMetadata: err = %s, req = %#v", err, tc.req) + } + + if !tc.ok && err == nil { + t.Fatalf("expected to fail, but success: req = %#v, got = %#v", tc.req, got) + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("got=%#v, but want=%#v", got, tc.want) + } + }) + } +} + +const mockProviderPackageMetadataResponse = `{ + "protocols": [ + "5.0" + ], + "os": "darwin", + "arch": "arm64", + "filename": "terraform-provider-null_3.2.1_darwin_arm64.zip", + "download_url": "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_darwin_arm64.zip", + "shasums_url": "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_SHA256SUMS", + "shasums_signature_url": "https://releases.hashicorp.com/terraform-provider-null/3.2.1/terraform-provider-null_3.2.1_SHA256SUMS.72D7468F.sig", + "shasum": "e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "signing_keys": { + "gpg_public_keys": [ + { + "key_id": "34365D9472D7468F", + "ascii_armor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGB9+xkBEACabYZOWKmgZsHTdRDiyPJxhbuUiKX65GUWkyRMJKi/1dviVxOX\nPG6hBPtF48IFnVgxKpIb7G6NjBousAV+CuLlv5yqFKpOZEGC6sBV+Gx8Vu1CICpl\nZm+HpQPcIzwBpN+Ar4l/exCG/f/MZq/oxGgH+TyRF3XcYDjG8dbJCpHO5nQ5Cy9h\nQIp3/Bh09kET6lk+4QlofNgHKVT2epV8iK1cXlbQe2tZtfCUtxk+pxvU0UHXp+AB\n0xc3/gIhjZp/dePmCOyQyGPJbp5bpO4UeAJ6frqhexmNlaw9Z897ltZmRLGq1p4a\nRnWL8FPkBz9SCSKXS8uNyV5oMNVn4G1obCkc106iWuKBTibffYQzq5TG8FYVJKrh\nRwWB6piacEB8hl20IIWSxIM3J9tT7CPSnk5RYYCTRHgA5OOrqZhC7JefudrP8n+M\npxkDgNORDu7GCfAuisrf7dXYjLsxG4tu22DBJJC0c/IpRpXDnOuJN1Q5e/3VUKKW\nmypNumuQpP5lc1ZFG64TRzb1HR6oIdHfbrVQfdiQXpvdcFx+Fl57WuUraXRV6qfb\n4ZmKHX1JEwM/7tu21QE4F1dz0jroLSricZxfaCTHHWNfvGJoZ30/MZUrpSC0IfB3\niQutxbZrwIlTBt+fGLtm3vDtwMFNWM+Rb1lrOxEQd2eijdxhvBOHtlIcswARAQAB\ntERIYXNoaUNvcnAgU2VjdXJpdHkgKGhhc2hpY29ycC5jb20vc2VjdXJpdHkpIDxz\nZWN1cml0eUBoYXNoaWNvcnAuY29tPokCVAQTAQoAPhYhBMh0AR8KtAURDQIQVTQ2\nXZRy10aPBQJgffsZAhsDBQkJZgGABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ\nEDQ2XZRy10aPtpcP/0PhJKiHtC1zREpRTrjGizoyk4Sl2SXpBZYhkdrG++abo6zs\nbuaAG7kgWWChVXBo5E20L7dbstFK7OjVs7vAg/OLgO9dPD8n2M19rpqSbbvKYWvp\n0NSgvFTT7lbyDhtPj0/bzpkZEhmvQaDWGBsbDdb2dBHGitCXhGMpdP0BuuPWEix+\nQnUMaPwU51q9GM2guL45Tgks9EKNnpDR6ZdCeWcqo1IDmklloidxT8aKL21UOb8t\ncD+Bg8iPaAr73bW7Jh8TdcV6s6DBFub+xPJEB/0bVPmq3ZHs5B4NItroZ3r+h3ke\nVDoSOSIZLl6JtVooOJ2la9ZuMqxchO3mrXLlXxVCo6cGcSuOmOdQSz4OhQE5zBxx\nLuzA5ASIjASSeNZaRnffLIHmht17BPslgNPtm6ufyOk02P5XXwa69UCjA3RYrA2P\nQNNC+OWZ8qQLnzGldqE4MnRNAxRxV6cFNzv14ooKf7+k686LdZrP/3fQu2p3k5rY\n0xQUXKh1uwMUMtGR867ZBYaxYvwqDrg9XB7xi3N6aNyNQ+r7zI2lt65lzwG1v9hg\nFG2AHrDlBkQi/t3wiTS3JOo/GCT8BjN0nJh0lGaRFtQv2cXOQGVRW8+V/9IpqEJ1\nqQreftdBFWxvH7VJq2mSOXUJyRsoUrjkUuIivaA9Ocdipk2CkP8bpuGz7ZF4uQIN\nBGB9+xkBEACoklYsfvWRCjOwS8TOKBTfl8myuP9V9uBNbyHufzNETbhYeT33Cj0M\nGCNd9GdoaknzBQLbQVSQogA+spqVvQPz1MND18GIdtmr0BXENiZE7SRvu76jNqLp\nKxYALoK2Pc3yK0JGD30HcIIgx+lOofrVPA2dfVPTj1wXvm0rbSGA4Wd4Ng3d2AoR\nG/wZDAQ7sdZi1A9hhfugTFZwfqR3XAYCk+PUeoFrkJ0O7wngaon+6x2GJVedVPOs\n2x/XOR4l9ytFP3o+5ILhVnsK+ESVD9AQz2fhDEU6RhvzaqtHe+sQccR3oVLoGcat\nma5rbfzH0Fhj0JtkbP7WreQf9udYgXxVJKXLQFQgel34egEGG+NlbGSPG+qHOZtY\n4uWdlDSvmo+1P95P4VG/EBteqyBbDDGDGiMs6lAMg2cULrwOsbxWjsWka8y2IN3z\n1stlIJFvW2kggU+bKnQ+sNQnclq3wzCJjeDBfucR3a5WRojDtGoJP6Fc3luUtS7V\n5TAdOx4dhaMFU9+01OoH8ZdTRiHZ1K7RFeAIslSyd4iA/xkhOhHq89F4ECQf3Bt4\nZhGsXDTaA/VgHmf3AULbrC94O7HNqOvTWzwGiWHLfcxXQsr+ijIEQvh6rHKmJK8R\n9NMHqc3L18eMO6bqrzEHW0Xoiu9W8Yj+WuB3IKdhclT3w0pO4Pj8gQARAQABiQI8\nBBgBCgAmFiEEyHQBHwq0BRENAhBVNDZdlHLXRo8FAmB9+xkCGwwFCQlmAYAACgkQ\nNDZdlHLXRo9ZnA/7BmdpQLeTjEiXEJyW46efxlV1f6THn9U50GWcE9tebxCXgmQf\nu+Uju4hreltx6GDi/zbVVV3HCa0yaJ4JVvA4LBULJVe3ym6tXXSYaOfMdkiK6P1v\nJgfpBQ/b/mWB0yuWTUtWx18BQQwlNEQWcGe8n1lBbYsH9g7QkacRNb8tKUrUbWlQ\nQsU8wuFgly22m+Va1nO2N5C/eE/ZEHyN15jEQ+QwgQgPrK2wThcOMyNMQX/VNEr1\nY3bI2wHfZFjotmek3d7ZfP2VjyDudnmCPQ5xjezWpKbN1kvjO3as2yhcVKfnvQI5\nP5Frj19NgMIGAp7X6pF5Csr4FX/Vw316+AFJd9Ibhfud79HAylvFydpcYbvZpScl\n7zgtgaXMCVtthe3GsG4gO7IdxxEBZ/Fm4NLnmbzCIWOsPMx/FxH06a539xFq/1E2\n1nYFjiKg8a5JFmYU/4mV9MQs4bP/3ip9byi10V+fEIfp5cEEmfNeVeW5E7J8PqG9\nt4rLJ8FR4yJgQUa2gs2SNYsjWQuwS/MJvAv4fDKlkQjQmYRAOp1SszAnyaplvri4\nncmfDsf0r65/sd6S40g5lHH8LIbGxcOIN6kwthSTPWX89r42CbY8GzjTkaeejNKx\nv1aCrO58wAtursO1DiXCvBY7+NdafMRnoHwBk50iPqrVkNA8fv+auRyB2/G5Ag0E\nYH3+JQEQALivllTjMolxUW2OxrXb+a2Pt6vjCBsiJzrUj0Pa63U+lT9jldbCCfgP\nwDpcDuO1O05Q8k1MoYZ6HddjWnqKG7S3eqkV5c3ct3amAXp513QDKZUfIDylOmhU\nqvxjEgvGjdRjz6kECFGYr6Vnj/p6AwWv4/FBRFlrq7cnQgPynbIH4hrWvewp3Tqw\nGVgqm5RRofuAugi8iZQVlAiQZJo88yaztAQ/7VsXBiHTn61ugQ8bKdAsr8w/ZZU5\nHScHLqRolcYg0cKN91c0EbJq9k1LUC//CakPB9mhi5+aUVUGusIM8ECShUEgSTCi\nKQiJUPZ2CFbbPE9L5o9xoPCxjXoX+r7L/WyoCPTeoS3YRUMEnWKvc42Yxz3meRb+\nBmaqgbheNmzOah5nMwPupJYmHrjWPkX7oyyHxLSFw4dtoP2j6Z7GdRXKa2dUYdk2\nx3JYKocrDoPHh3Q0TAZujtpdjFi1BS8pbxYFb3hHmGSdvz7T7KcqP7ChC7k2RAKO\nGiG7QQe4NX3sSMgweYpl4OwvQOn73t5CVWYp/gIBNZGsU3Pto8g27vHeWyH9mKr4\ncSepDhw+/X8FGRNdxNfpLKm7Vc0Sm9Sof8TRFrBTqX+vIQupYHRi5QQCuYaV6OVr\nITeegNK3So4m39d6ajCR9QxRbmjnx9UcnSYYDmIB6fpBuwT0ogNtABEBAAGJBHIE\nGAEKACYCGwIWIQTIdAEfCrQFEQ0CEFU0Nl2UctdGjwUCYH4bgAUJAeFQ2wJAwXQg\nBBkBCgAdFiEEs2y6kaLAcwxDX8KAsLRBCXaFtnYFAmB9/iUACgkQsLRBCXaFtnYX\nBhAAlxejyFXoQwyGo9U+2g9N6LUb/tNtH29RHYxy4A3/ZUY7d/FMkArmh4+dfjf0\np9MJz98Zkps20kaYP+2YzYmaizO6OA6RIddcEXQDRCPHmLts3097mJ/skx9qLAf6\nrh9J7jWeSqWO6VW6Mlx8j9m7sm3Ae1OsjOx/m7lGZOhY4UYfY627+Jf7WQ5103Qs\nlgQ09es/vhTCx0g34SYEmMW15Tc3eCjQ21b1MeJD/V26npeakV8iCZ1kHZHawPq/\naCCuYEcCeQOOteTWvl7HXaHMhHIx7jjOd8XX9V+UxsGz2WCIxX/j7EEEc7CAxwAN\nnWp9jXeLfxYfjrUB7XQZsGCd4EHHzUyCf7iRJL7OJ3tz5Z+rOlNjSgci+ycHEccL\nYeFAEV+Fz+sj7q4cFAferkr7imY1XEI0Ji5P8p/uRYw/n8uUf7LrLw5TzHmZsTSC\nUaiL4llRzkDC6cVhYfqQWUXDd/r385OkE4oalNNE+n+txNRx92rpvXWZ5qFYfv7E\n95fltvpXc0iOugPMzyof3lwo3Xi4WZKc1CC/jEviKTQhfn3WZukuF5lbz3V1PQfI\nxFsYe9WYQmp25XGgezjXzp89C/OIcYsVB1KJAKihgbYdHyUN4fRCmOszmOUwEAKR\n3k5j4X8V5bk08sA69NVXPn2ofxyk3YYOMYWW8ouObnXoS8QJEDQ2XZRy10aPMpsQ\nAIbwX21erVqUDMPn1uONP6o4NBEq4MwG7d+fT85rc1U0RfeKBwjucAE/iStZDQoM\nZKWvGhFR+uoyg1LrXNKuSPB82unh2bpvj4zEnJsJadiwtShTKDsikhrfFEK3aCK8\nZuhpiu3jxMFDhpFzlxsSwaCcGJqcdwGhWUx0ZAVD2X71UCFoOXPjF9fNnpy80YNp\nflPjj2RnOZbJyBIM0sWIVMd8F44qkTASf8K5Qb47WFN5tSpePq7OCm7s8u+lYZGK\nwR18K7VliundR+5a8XAOyUXOL5UsDaQCK4Lj4lRaeFXunXl3DJ4E+7BKzZhReJL6\nEugV5eaGonA52TWtFdB8p+79wPUeI3KcdPmQ9Ll5Zi/jBemY4bzasmgKzNeMtwWP\nfk6WgrvBwptqohw71HDymGxFUnUP7XYYjic2sVKhv9AevMGycVgwWBiWroDCQ9Ja\nbtKfxHhI2p+g+rcywmBobWJbZsujTNjhtme+kNn1mhJsD3bKPjKQfAxaTskBLb0V\nwgV21891TS1Dq9kdPLwoS4XNpYg2LLB4p9hmeG3fu9+OmqwY5oKXsHiWc43dei9Y\nyxZ1AAUOIaIdPkq+YG/PhlGE4YcQZ4RPpltAr0HfGgZhmXWigbGS+66pUj+Ojysc\nj0K5tCVxVu0fhhFpOlHv0LWaxCbnkgkQH9jfMEJkAWMOuQINBGCAXCYBEADW6RNr\nZVGNXvHVBqSiOWaxl1XOiEoiHPt50Aijt25yXbG+0kHIFSoR+1g6Lh20JTCChgfQ\nkGGjzQvEuG1HTw07YhsvLc0pkjNMfu6gJqFox/ogc53mz69OxXauzUQ/TZ27GDVp\nUBu+EhDKt1s3OtA6Bjz/csop/Um7gT0+ivHyvJ/jGdnPEZv8tNuSE/Uo+hn/Q9hg\n8SbveZzo3C+U4KcabCESEFl8Gq6aRi9vAfa65oxD5jKaIz7cy+pwb0lizqlW7H9t\nQlr3dBfdIcdzgR55hTFC5/XrcwJ6/nHVH/xGskEasnfCQX8RYKMuy0UADJy72TkZ\nbYaCx+XXIcVB8GTOmJVoAhrTSSVLAZspfCnjwnSxisDn3ZzsYrq3cV6sU8b+QlIX\n7VAjurE+5cZiVlaxgCjyhKqlGgmonnReWOBacCgL/UvuwMmMp5TTLmiLXLT7uxeG\nojEyoCk4sMrqrU1jevHyGlDJH9Taux15GILDwnYFfAvPF9WCid4UZ4Ouwjcaxfys\n3LxNiZIlUsXNKwS3mhiMRL4TRsbs4k4QE+LIMOsauIvcvm8/frydvQ/kUwIhVTH8\n0XGOH909bYtJvY3fudK7ShIwm7ZFTduBJUG473E/Fn3VkhTmBX6+PjOC50HR/Hyb\nwaRCzfDruMe3TAcE/tSP5CUOb9C7+P+hPzQcDwARAQABiQRyBBgBCgAmFiEEyHQB\nHwq0BRENAhBVNDZdlHLXRo8FAmCAXCYCGwIFCQlmAYACQAkQNDZdlHLXRo/BdCAE\nGQEKAB0WIQQ3TsdbSFkTYEqDHMfIIMbVzSerhwUCYIBcJgAKCRDIIMbVzSerh0Xw\nD/9ghnUsoNCu1OulcoJdHboMazJvDt/znttdQSnULBVElgM5zk0Uyv87zFBzuCyQ\nJWL3bWesQ2uFx5fRWEPDEfWVdDrjpQGb1OCCQyz1QlNPV/1M1/xhKGS9EeXrL8Dw\nF6KTGkRwn1yXiP4BGgfeFIQHmJcKXEZ9HkrpNb8mcexkROv4aIPAwn+IaE+NHVtt\nIBnufMXLyfpkWJQtJa9elh9PMLlHHnuvnYLvuAoOkhuvs7fXDMpfFZ01C+QSv1dz\nHm52GSStERQzZ51w4c0rYDneYDniC/sQT1x3dP5Xf6wzO+EhRMabkvoTbMqPsTEP\nxyWr2pNtTBYp7pfQjsHxhJpQF0xjGN9C39z7f3gJG8IJhnPeulUqEZjhRFyVZQ6/\nsiUeq7vu4+dM/JQL+i7KKe7Lp9UMrG6NLMH+ltaoD3+lVm8fdTUxS5MNPoA/I8cK\n1OWTJHkrp7V/XaY7mUtvQn5V1yET5b4bogz4nME6WLiFMd+7x73gB+YJ6MGYNuO8\ne/NFK67MfHbk1/AiPTAJ6s5uHRQIkZcBPG7y5PpfcHpIlwPYCDGYlTajZXblyKrw\nBttVnYKvKsnlysv11glSg0DphGxQJbXzWpvBNyhMNH5dffcfvd3eXJAxnD81GD2z\nZAriMJ4Av2TfeqQ2nxd2ddn0jX4WVHtAvLXfCgLM2Gveho4jD/9sZ6PZz/rEeTvt\nh88t50qPcBa4bb25X0B5FO3TeK2LL3VKLuEp5lgdcHVonrcdqZFobN1CgGJua8TW\nSprIkh+8ATZ/FXQTi01NzLhHXT1IQzSpFaZw0gb2f5ruXwvTPpfXzQrs2omY+7s7\nfkCwGPesvpSXPKn9v8uhUwD7NGW/Dm+jUM+QtC/FqzX7+/Q+OuEPjClUh1cqopCZ\nEvAI3HjnavGrYuU6DgQdjyGT/UDbuwbCXqHxHojVVkISGzCTGpmBcQYQqhcFRedJ\nyJlu6PSXlA7+8Ajh52oiMJ3ez4xSssFgUQAyOB16432tm4erpGmCyakkoRmMUn3p\nwx+QIppxRlsHznhcCQKR3tcblUqH3vq5i4/ZAihusMCa0YrShtxfdSb13oKX+pFr\naZXvxyZlCa5qoQQBV1sowmPL1N2j3dR9TVpdTyCFQSv4KeiExmowtLIjeCppRBEK\neeYHJnlfkyKXPhxTVVO6H+dU4nVu0ASQZ07KiQjbI+zTpPKFLPp3/0sPRJM57r1+\naTS71iR7nZNZ1f8LZV2OvGE6fJVtgJ1J4Nu02K54uuIhU3tg1+7Xt+IqwRc9rbVr\npHH/hFCYBPW2D2dxB+k2pQlg5NI+TpsXj5Zun8kRw5RtVb+dLuiH/xmxArIee8Jq\nZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==\n=7pIB\n-----END PGP PUBLIC KEY BLOCK-----", + "trust_signature": "", + "source": "HashiCorp", + "source_url": "https://www.hashicorp.com/security.html" + } + ] + } +} +` diff --git a/tfupdate/context.go b/tfupdate/context.go new file mode 100644 index 0000000..c10cfb4 --- /dev/null +++ b/tfupdate/context.go @@ -0,0 +1,173 @@ +package tfupdate + +import ( + "fmt" + "log" + + version "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-config-inspect/tfconfig" + "github.com/spf13/afero" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// GlobalContext is information that is shared over the lifetime of the process. +type GlobalContext struct { + // fs is an afero filesystem for testing. + fs afero.Fs + + // updater is an interface to rewriting rule implementations. + updater Updater + + // option is a set of global parameters. + option Option +} + +// NewGlobalContext returns a new instance of NewGlobalContext. +func NewGlobalContext(fs afero.Fs, o Option) (*GlobalContext, error) { + updater, err := NewUpdater(o) + if err != nil { + return nil, err + } + + gc := &GlobalContext{ + fs: fs, + updater: updater, + option: o, + } + + return gc, nil +} + +// ModuleContext is information shared across files within a directory. +type ModuleContext struct { + // gc is a pointer to delegate some implementations to GlobalContext. + gc *GlobalContext + + // dir is a relative path to the module from the current working directory. + dir string + + // requiredProviders is version constraints of Terraform providers. + // This is the result of parsing terraform-config-inspect and may contain + // multiple constraints. The meaning depends on the use case and is therefore + // lazily evaluated. + requiredProviders map[string]*tfconfig.ProviderRequirement +} + +// SelectedProvider is the source address and version of the provider, as +// inferred from the version constraint. +type SelectedProvider struct { + // source is a source address of the provider. + Source string + + // version is a version of the provider. + Version string +} + +// aferoToTfconfigFS converts afero.Fs to tfconfig.FS. +// The filesystem has been replaced for testing purposes, but due to historical +// reasons, we use afero.Fs instead of standard io/fs.Fs introduced in Go 1.16. +// On the other hand, the tfconfig uses its own tfconfig.FS, which is also +// incompatible the standard one. Fortunately, both have adaptors for +// converting the interface to the standard one. Converting afero.Fs to +// io/fs.FS and then to tfconfig.FS makes the types match. +func aferoToTfconfigFS(afs afero.Fs) tfconfig.FS { + return tfconfig.WrapFS(afero.NewIOFS(afs)) +} + +// NewModuleContext parses a given module and returns a new ModuleContext. +// The dir is a relative path to the module from the current working directory. +func NewModuleContext(dir string, gc *GlobalContext) (*ModuleContext, error) { + m, diags := tfconfig.LoadModuleFromFilesystem(aferoToTfconfigFS(gc.fs), dir) + if diags.HasErrors() { + return nil, fmt.Errorf("failed to load module: dir = %s, err = %s", dir, diags) + } + + c := &ModuleContext{ + gc: gc, + dir: dir, + requiredProviders: m.RequiredProviders, + } + + return c, nil +} + +// GlobalContext returns an instance of the global context. +func (mc *ModuleContext) GlobalContext() *GlobalContext { + return mc.gc +} + +// FS returns an instance of afero filesystem +func (mc *ModuleContext) FS() afero.Fs { + return mc.gc.fs +} + +// Updater returns an instance of Updater. +func (mc *ModuleContext) Updater() Updater { + return mc.gc.updater +} + +// Option returns an instance of Option. +func (mc *ModuleContext) Option() Option { + return mc.gc.option +} + +// SelectedProviders returns a list of providers inferred from version constraints. +// The result is sorted alphabetically by source address. +// Version constraints only support simple constants and not comparison +// operators. Ignore what cannot be interpreted. +func (mc *ModuleContext) SelecetedProviders() []SelectedProvider { + selected := make(map[string]string) + for _, p := range mc.requiredProviders { + if p.Source == "" { + // A source address with an empty string implies an unknown namespace prior to + // Terraform v0.13, but since this is already a deprecated usage, we don't + // implicitly complement the official hashicorp namespace and is not included + // in the results. + log.Printf("[DEBUG] ModuleContext.SelecetedProviders: ignore legacy provider address notation: %s", p.Source) + continue + } + + v := selectVersion(p.VersionConstraints) + + if v == "" { + // Ignore if no version is specified. + log.Printf("[DEBUG] ModuleContext.SelecetedProviders: ignore no version selected: %s", p.Source) + continue + } + + // It is not possible to mix multiple provider versions in one module, so + // simply overwrite without taking duplicates into account + selected[p.Source] = v + } + + // Sort to get stable results + keys := maps.Keys(selected) + slices.Sort(keys) + + ret := []SelectedProvider{} + for _, k := range keys { + s := SelectedProvider{Source: k, Version: selected[k]} + ret = append(ret, s) + } + return ret +} + +// selectVersion resolves version constraints and returns the version. +// Note that it does not actually re-implement the resolution of version +// constraints in terraform init. It is very simplified for the use we need. +// Version constraints only support simple constants and not comparison +// operators. Ignore what cannot be interpreted. +func selectVersion(constraints []string) string { + for _, c := range constraints { + v, err := version.NewVersion(c) + if err != nil { + // Ignore parse error + log.Printf("[DEBUG] selectVersion: ignore version parse error: constaraints = %#v, err = %s", constraints, err) + continue + } + // return the first one found + return v.String() + } + return "" +} diff --git a/tfupdate/context_test.go b/tfupdate/context_test.go new file mode 100644 index 0000000..ab8f6a9 --- /dev/null +++ b/tfupdate/context_test.go @@ -0,0 +1,218 @@ +package tfupdate + +import ( + "os" + "path/filepath" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/spf13/afero" +) + +func TestModuleContextSelecetedProviders(t *testing.T) { + cases := []struct { + desc string + src string + want []SelectedProvider + }{ + { + desc: "simple", + src: ` +terraform { + required_version = "1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + source = "hashicorp/null" + version = "3.1.1" + } + + github = { + source = "integrations/github" + version = "4.28.0" + } + } +} +`, + want: []SelectedProvider{ + SelectedProvider{Source: "hashicorp/aws", Version: "5.4.0"}, + SelectedProvider{Source: "hashicorp/null", Version: "3.1.1"}, + SelectedProvider{Source: "integrations/github", Version: "4.28.0"}, + }, + }, + { + desc: "empty", + src: ` +terraform { + required_version = "1.5.0" +} +`, + want: []SelectedProvider{}, + }, + { + desc: "unknown source", + src: ` +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + version = "3.1.1" + } + } +} +`, + want: []SelectedProvider{ + SelectedProvider{Source: "hashicorp/aws", Version: "5.4.0"}, + }, + }, + { + desc: "unknown version", + src: ` +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + source = "hashicorp/null" + } + } +} +`, + want: []SelectedProvider{ + SelectedProvider{Source: "hashicorp/aws", Version: "5.4.0"}, + }, + }, + { + desc: "provider block (unknown source)", + src: ` +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + } +} + +provider "null" { + version = "3.2.1" +} +`, + want: []SelectedProvider{ + SelectedProvider{Source: "hashicorp/aws", Version: "5.4.0"}, + }, + }, + { + desc: "version constraint", + src: ` +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + source = "hashicorp/null" + version = "> 3.0.0" + } + } +} +`, + want: []SelectedProvider{ + SelectedProvider{Source: "hashicorp/aws", Version: "5.4.0"}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + fs := afero.NewMemMapFs() + dirname := "test" + err := fs.MkdirAll(dirname, os.ModePerm) + if err != nil { + t.Fatalf("failed to create dir: %s", err) + } + err = afero.WriteFile(fs, filepath.Join(dirname, "main.tf"), []byte(tc.src), 0644) + if err != nil { + t.Fatalf("failed to write file: %s", err) + } + + gc := &GlobalContext{ + fs: fs, + } + mc, err := NewModuleContext(dirname, gc) + if err != nil { + t.Fatalf("failed to new ModuleContext: %s", err) + } + + got := mc.SelecetedProviders() + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.want), diff) + } + }) + } +} + +func TestSelectVersion(t *testing.T) { + cases := []struct { + desc string + constraints []string + want string + }{ + { + desc: "simple", + constraints: []string{"3.2.1"}, + want: "3.2.1", + }, + { + desc: "empty list", + constraints: []string{}, + want: "", + }, + { + desc: "empty string", + constraints: []string{""}, + want: "", + }, + { + desc: "return first one found", + constraints: []string{"1.2.3", "3.2.1"}, + want: "1.2.3", + }, + { + desc: "ignore parse error", + constraints: []string{"> 1.2.3"}, + want: "", + }, + { + desc: "ignore parse error and return first one found", + constraints: []string{"= 1.2.3", "3.2.1"}, + want: "3.2.1", + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + got := selectVersion(tc.constraints) + + if got != tc.want { + t.Errorf("got=%s, but want=%s", got, tc.want) + } + }) + } +} diff --git a/tfupdate/file.go b/tfupdate/file.go index 7d3342a..c36ea2f 100644 --- a/tfupdate/file.go +++ b/tfupdate/file.go @@ -2,6 +2,7 @@ package tfupdate import ( "bytes" + "context" "fmt" "log" "os" @@ -14,16 +15,16 @@ import ( // UpdateFile updates version constraints in a single file. // We use an afero filesystem here for testing. -func UpdateFile(fs afero.Fs, filename string, o Option) error { +func UpdateFile(ctx context.Context, mc *ModuleContext, filename string) error { log.Printf("[DEBUG] check file: %s", filename) - r, err := fs.Open(filename) + r, err := mc.FS().Open(filename) if err != nil { return fmt.Errorf("failed to open file: %s", err) } defer r.Close() w := &bytes.Buffer{} - isUpdated, err := UpdateHCL(r, w, filename, o) + isUpdated, err := UpdateHCL(ctx, mc, r, w, filename) if err != nil { return err } @@ -37,7 +38,7 @@ func UpdateFile(fs afero.Fs, filename string, o Option) error { // does not seem to preserve an original SpaceBefore value of attribute. // So, we need to format output here. result := hclwrite.Format(updated) - if err = afero.WriteFile(fs, filename, result, os.ModePerm); err != nil { + if err = afero.WriteFile(mc.FS(), filename, result, os.ModePerm); err != nil { return fmt.Errorf("failed to write file: %s", err) } } @@ -48,10 +49,11 @@ func UpdateFile(fs afero.Fs, filename string, o Option) error { // UpdateDir updates version constraints for files in a given directory. // If a recursive flag is true, it checks and updates recursively. // skip hidden directories such as .terraform or .git. -// It also skips a file without .tf extension. -func UpdateDir(fs afero.Fs, dirname string, o Option) error { +// It also skips unsupported file type. +func UpdateDir(ctx context.Context, current *ModuleContext, dirname string) error { log.Printf("[DEBUG] check dir: %s", dirname) - dir, err := afero.ReadDir(fs, dirname) + option := current.Option() + dir, err := afero.ReadDir(current.FS(), dirname) if err != nil { return fmt.Errorf("failed to open dir: %s", err) } @@ -60,14 +62,14 @@ func UpdateDir(fs afero.Fs, dirname string, o Option) error { path := filepath.Join(dirname, entry.Name()) // if a path of entry matches ignorePaths, skip it. - if o.MatchIgnorePaths(path) { + if option.MatchIgnorePaths(path) { log.Printf("[DEBUG] ignore: %s", path) continue } if entry.IsDir() { // if an entry is a directory - if !o.recursive { + if !option.recursive { // skip directory if a recursive flag is false continue } @@ -76,7 +78,12 @@ func UpdateDir(fs afero.Fs, dirname string, o Option) error { continue } - err := UpdateDir(fs, path, o) + child, err := NewModuleContext(path, current.GlobalContext()) + if err != nil { + return err + } + + err = UpdateDir(ctx, child, path) if err != nil { return err } @@ -85,12 +92,12 @@ func UpdateDir(fs afero.Fs, dirname string, o Option) error { } // if an entry is a file - if filepath.Ext(entry.Name()) != ".tf" { - // skip a file without .tf extension. + if !(filepath.Ext(entry.Name()) == ".tf" || entry.Name() == ".terraform.lock.hcl") { + // skip unsupported file type continue } - err := UpdateFile(fs, path, o) + err := UpdateFile(ctx, current, path) if err != nil { return err } @@ -99,17 +106,28 @@ func UpdateDir(fs afero.Fs, dirname string, o Option) error { } // UpdateFileOrDir updates version constraints in a given file or directory. -func UpdateFileOrDir(fs afero.Fs, path string, o Option) error { - isDir, err := afero.IsDir(fs, path) +func UpdateFileOrDir(ctx context.Context, gc *GlobalContext, path string) error { + isDir, err := afero.IsDir(gc.fs, path) if err != nil { return fmt.Errorf("failed to open path: %s", err) } if isDir { // if an entry is a directory - return UpdateDir(fs, path, o) + mc, err := NewModuleContext(path, gc) + if err != nil { + return err + } + return UpdateDir(ctx, mc, path) } // if an entry is a file - return UpdateFile(fs, path, o) + // Note that even if only the filename is specified, the directory containing + // it is read for module context analysis. + dir := filepath.Dir(path) + mc, err := NewModuleContext(dir, gc) + if err != nil { + return err + } + return UpdateFile(ctx, mc, path) } diff --git a/tfupdate/file_test.go b/tfupdate/file_test.go index dca9848..93cf3f0 100644 --- a/tfupdate/file_test.go +++ b/tfupdate/file_test.go @@ -1,6 +1,7 @@ package tfupdate import ( + "context" "os" "path/filepath" "regexp" @@ -35,24 +36,6 @@ terraform { `, ok: true, }, - { - filename: "invalid.tf", - src: ` -terraform { - required_version = "0.12.6" -} -`, - o: Option{ - updateType: "hoge", - version: "0.12.7", - }, - want: ` -terraform { - required_version = "0.12.6" -} -`, - ok: false, - }, { filename: "unformatted_match.tf", src: ` @@ -98,7 +81,17 @@ required_version = "0.12.6" t.Fatalf("failed to write file: %s", err) } - err = UpdateFile(fs, tc.filename, tc.o) + gc, err := NewGlobalContext(fs, tc.o) + if err != nil { + t.Fatalf("failed to new global context: %s", err) + } + + mc, err := NewModuleContext(".", gc) + if err != nil { + t.Fatalf("failed to new module context: %s", err) + } + + err = UpdateFile(context.Background(), mc, tc.filename) if tc.ok && err != nil { t.Errorf("UpdateFile() with filename = %s, o = %#v returns unexpected err: %+v", tc.filename, tc.o, err) } @@ -121,9 +114,22 @@ required_version = "0.12.6" func TestUpdateFileNotFound(t *testing.T) { fs := afero.NewMemMapFs() filename := "not_found.tf" - o := Option{} + o := Option{ + updateType: "terraform", + version: "0.12.7", + } + + gc, err := NewGlobalContext(fs, o) + if err != nil { + t.Fatalf("failed to new global context: %s", err) + } + + mc, err := NewModuleContext(".", gc) + if err != nil { + t.Fatalf("failed to new module context: %s", err) + } - err := UpdateFile(fs, filename, o) + err = UpdateFile(context.Background(), mc, filename) if err == nil { t.Errorf("UpdateFile() with filename = %s, o = %#v expects to return an error, but no error", filename, o) @@ -388,7 +394,17 @@ terraform { t.Fatalf("failed to write file: %s", err) } - err = UpdateDir(fs, tc.checkdir, tc.o) + gc, err := NewGlobalContext(fs, tc.o) + if err != nil { + t.Fatalf("failed to new global context: %s", err) + } + + mc, err := NewModuleContext(dirname, gc) + if err != nil { + t.Fatalf("failed to new module context: %s", err) + } + + err = UpdateDir(context.Background(), mc, tc.checkdir) if err != nil { t.Errorf("UpdateDir() with dirname = %s, o = %#v returns an unexpected error: %+v", tc.checkdir, tc.o, err) @@ -414,18 +430,6 @@ terraform { } } -func TestUpdateDirNotFound(t *testing.T) { - fs := afero.NewMemMapFs() - dirname := "not_found" - o := Option{} - - err := UpdateDir(fs, dirname, o) - - if err == nil { - t.Errorf("UpdateDir() with dirname = %s, o = %#v expects to return an error, but no error", dirname, o) - } -} - func TestUpdateFileOrDirFile(t *testing.T) { filename := "terraform.tf" src := ` @@ -467,7 +471,12 @@ terraform { t.Fatalf("failed to write file: %s", err) } - err = UpdateFileOrDir(fs, tc.path, o) + gc, err := NewGlobalContext(fs, o) + if err != nil { + t.Fatalf("failed to new global context: %s", err) + } + + err = UpdateFileOrDir(context.Background(), gc, tc.path) if err != nil { t.Errorf("UpdateFileOrDir() with path = %s, o = %#v returns an unexpected error: %+v", tc.path, o, err) diff --git a/tfupdate/hclwrite.go b/tfupdate/hclwrite.go index 50ecf3d..47e17fd 100644 --- a/tfupdate/hclwrite.go +++ b/tfupdate/hclwrite.go @@ -3,10 +3,13 @@ package tfupdate import ( "fmt" "reflect" + "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclparse" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" ) // allMatchingBlocks returns all matching blocks from the body that have the @@ -81,3 +84,41 @@ func getHCLNativeAttribute(body *hclwrite.Body, name string) (*hcl.Attribute, er return hclAttr, nil } + +// getAttributeValueAsString returns a value of Attribute as string. +// There is no way to get value as string directly, +// so we parses tokens of Attribute and build string representation. +// The returned value is unquoted. +func getAttributeValueAsUnquotedString(attr *hclwrite.Attribute) string { + // find TokenEqual + expr := attr.Expr() + exprTokens := expr.BuildTokens(nil) + + // TokenIdent records SpaceBefore, but we should ignore it here. + quotedValue := strings.TrimSpace(string(exprTokens.Bytes())) + + // unquote + value := strings.Trim(quotedValue, "\"") + + return value +} + +// tokensForListPerLine builds a hclwrite.Tokens for a given list, but breaks the line for each element. +func tokensForListPerLine(list []string) hclwrite.Tokens { + // The original TokensForValue implementation does not break line by line for list, + // so we build a token sequence by ourselves. + tokens := hclwrite.Tokens{} + tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}}) + tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}}) + + for _, i := range list { + ts := hclwrite.TokensForValue(cty.StringVal(i)) + tokens = append(tokens, ts...) + tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte{','}}) + tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}}) + } + + tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}}) + + return tokens +} diff --git a/tfupdate/hclwrite_test.go b/tfupdate/hclwrite_test.go index f5e54e6..5bd795e 100644 --- a/tfupdate/hclwrite_test.go +++ b/tfupdate/hclwrite_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" @@ -246,3 +247,69 @@ foo = "123" }) } } + +func TestGetAttributeValueAsUnquotedString(t *testing.T) { + cases := []struct { + desc string + src string + want string + }{ + { + desc: "simple", + src: ` +foo = "123" +`, + want: "123", + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + f, diags := hclwrite.ParseConfig([]byte(tc.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + attr := f.Body().GetAttribute("foo") + got := getAttributeValueAsUnquotedString(attr) + + if got != tc.want { + t.Errorf("got = %s, but want = %s", got, tc.want) + } + }) + } +} + +func TestTokensForListPerLine(t *testing.T) { + cases := []struct { + desc string + list []string + want string + }{ + { + desc: "simple", + list: []string{"aaa", "bbb"}, + want: `foo = [ + "aaa", + "bbb", +] +`, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + f := hclwrite.NewEmptyFile() + f.Body().SetAttributeRaw("foo", tokensForListPerLine(tc.list)) + + got := string(hclwrite.Format(f.BuildTokens(nil).Bytes())) + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", got, tc.want, diff) + } + }) + } +} diff --git a/tfupdate/lock.go b/tfupdate/lock.go new file mode 100644 index 0000000..fd6fd38 --- /dev/null +++ b/tfupdate/lock.go @@ -0,0 +1,132 @@ +package tfupdate + +import ( + "context" + "fmt" + "log" + "path/filepath" + + "github.com/hashicorp/hcl/v2/hclwrite" + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/minamijoyo/tfupdate/lock" + "github.com/zclconf/go-cty/cty" +) + +// LockUpdater is a updater implementation which updates the dependency lock file. +type LockUpdater struct { + platforms []string + + // index is a cached index for updating dependency lock files. + index lock.Index +} + +// NewLockUpdater is a factory method which returns an LockUpdater instance. +func NewLockUpdater(platforms []string, index lock.Index) (Updater, error) { + return &LockUpdater{ + platforms: platforms, + index: index, + }, nil +} + +// Update updates the dependency lock file. +// Note that this method will rewrite the AST passed as an argument. +func (u *LockUpdater) Update(ctx context.Context, mc *ModuleContext, filename string, f *hclwrite.File) error { + if filepath.Base(filename) != ".terraform.lock.hcl" { + // Skip other than the lock file. + return nil + } + + return u.updateLockfile(ctx, mc, f) +} + +// updateLockfile updates the dependency lock file. +func (u *LockUpdater) updateLockfile(ctx context.Context, mc *ModuleContext, f *hclwrite.File) error { + for _, p := range mc.SelecetedProviders() { + pAddr, err := fullyQualifiedProviderAddress(p.Source) + if err != nil { + // Unsupported formats, such as legacy abbreviated notation, will result + // in parse errors, but should be ignored without returning an error if + // possible. + log.Printf("[DEBUG] LockUpdater.updateLockfile: ignore legacy provider address notation: %s", p.Source) + continue + } + + pBlock := f.Body().FirstMatchingBlock("provider", []string{pAddr}) + if pBlock != nil { + // update the existing provider block + err := u.updateProviderBlock(ctx, pBlock, p) + if err != nil { + return err + } + } else { + // create a new provider block + f.Body().AppendNewline() + pBlock = f.Body().AppendNewBlock("provider", []string{pAddr}) + + err := u.updateProviderBlock(ctx, pBlock, p) + if err != nil { + return err + } + } + } + + return nil +} + +// updateProviderBlock updates the provider block in the dependency lock file. +func (u *LockUpdater) updateProviderBlock(ctx context.Context, pBlock *hclwrite.Block, p SelectedProvider) error { + vAttr := pBlock.Body().GetAttribute("version") + if vAttr != nil { + // a version attribute found + vVal := getAttributeValueAsUnquotedString(vAttr) + log.Printf("[DEBUG] check provider version in lock file: address = %s, lock = %s, config = %s", p.Source, vVal, p.Version) + if vVal == p.Version { + // Avoid unnecessary recalculations if no version change + return nil + } + } + + pBlock.Body().SetAttributeValue("version", cty.StringVal(p.Version)) + + //Strictly speaking, constraints can contain multiple constraint expressions, + //including comparison operators, but in the tfupdate use case, we assume + //that the required_providers are pinned to a specific version to detect the + //required version without terraform init, so we can simply specify the + //constraints attribute as the same as the version. This may differ from what + //terraform generates, but we expect that it doesn't matter in practice. + pBlock.Body().SetAttributeValue("constraints", cty.StringVal(p.Version)) + + // Calculate the hash value of the provider. + // Note that the provider will be downloaded if cache miss. + pv, err := u.index.GetOrCreateProviderVersion(ctx, p.Source, p.Version, u.platforms) + if err != nil { + return err + } + + hashes := pv.AllHashes() + pBlock.Body().SetAttributeRaw("hashes", tokensForListPerLine(hashes)) + + return nil +} + +// The financeQualifiedProviderAddress converts the short form of the provider +// address into the fully qualified form. +// hashicorp/null => registry.terraform.io/hashicorp/null +func fullyQualifiedProviderAddress(address string) (string, error) { + pAddr, err := tfaddr.ParseProviderSource(address) + if err != nil { + return "", fmt.Errorf("failed to parse provider aaddress: %s", address) + } + + // Since .terraform.lock.hcl was introduced from v0.14, we assume that + // provider address is qualified with namespaces at least. We won't support + // implicit legacy things. + if !pAddr.HasKnownNamespace() { + return "", fmt.Errorf("failed to parse unknown provider aaddress: %s", address) + } + if pAddr.IsLegacy() { + return "", fmt.Errorf("failed to parse legacy provider aaddress: %s", address) + } + + return pAddr.String(), nil +} diff --git a/tfupdate/lock_test.go b/tfupdate/lock_test.go new file mode 100644 index 0000000..712fe3b --- /dev/null +++ b/tfupdate/lock_test.go @@ -0,0 +1,1050 @@ +package tfupdate + +import ( + "context" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/minamijoyo/tfupdate/lock" + "github.com/spf13/afero" +) + +func TestNewLockUpdater(t *testing.T) { + index := lock.NewMockIndex([]*lock.ProviderVersion{}) + cases := []struct { + platforms []string + index lock.Index + want Updater + ok bool + }{ + { + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + index: index, + want: &LockUpdater{ + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + index: index, + }, + ok: true, + }, + } + + for _, tc := range cases { + got, err := NewLockUpdater(tc.platforms, tc.index) + if tc.ok && err != nil { + t.Errorf("NewLockUpdater() with platforms = %#v returns unexpected err: %+v", tc.platforms, err) + } + + if !tc.ok && err == nil { + t.Errorf("NewLockUpdater() with platforms = %#v expects to return an error, but no error", tc.platforms) + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("NewLockUpdater() with platforms = %s returns %#v, but want = %#v", tc.platforms, got, tc.want) + } + } +} + +func TestUpdateLock(t *testing.T) { + platforms := []string{"darwin_arm64", "darwin_amd64", "linux_amd64"} + pvs := []*lock.ProviderVersion{ + lock.NewMockProviderVersion( + "hashicorp/aws", + "5.4.0", + []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + map[string]string{ + "terraform-provider-aws_5.4.0_darwin_arm64.zip": "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "terraform-provider-aws_5.4.0_linux_amd64.zip": "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "terraform-provider-aws_5.4.0_darwin_amd64.zip": "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + }, + map[string]string{ + "terraform-provider-aws_5.4.0_freebsd_arm.zip": "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "terraform-provider-aws_5.4.0_windows_386.zip": "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "terraform-provider-aws_5.4.0_windows_amd64.zip": "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "terraform-provider-aws_5.4.0_openbsd_arm.zip": "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "terraform-provider-aws_5.4.0_linux_386.zip": "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "terraform-provider-aws_5.4.0_linux_arm64.zip": "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "terraform-provider-aws_5.4.0_darwin_arm64.zip": "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "terraform-provider-aws_5.4.0_darwin_amd64.zip": "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "terraform-provider-aws_5.4.0_openbsd_386.zip": "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "terraform-provider-aws_5.4.0_linux_arm.zip": "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "terraform-provider-aws_5.4.0_freebsd_amd64.zip": "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "terraform-provider-aws_5.4.0_freebsd_386.zip": "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "terraform-provider-aws_5.4.0_manifest.json": "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "terraform-provider-aws_5.4.0_openbsd_amd64.zip": "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "terraform-provider-aws_5.4.0_linux_amd64.zip": "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + }, + ), + lock.NewMockProviderVersion( + "hashicorp/null", + "3.2.1", + []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + map[string]string{ + "terraform-provider-null_3.2.1_linux_amd64.zip": "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "terraform-provider-null_3.2.1_darwin_amd64.zip": "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "terraform-provider-null_3.2.1_darwin_arm64.zip": "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + }, + map[string]string{ + "terraform-provider-null_3.2.1_freebsd_arm.zip": "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "terraform-provider-null_3.2.1_windows_386.zip": "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "terraform-provider-null_3.2.1_darwin_amd64.zip": "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "terraform-provider-null_3.2.1_linux_amd64.zip": "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "terraform-provider-null_3.2.1_manifest.json": "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "terraform-provider-null_3.2.1_windows_amd64.zip": "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "terraform-provider-null_3.2.1_freebsd_amd64.zip": "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "terraform-provider-null_3.2.1_linux_arm.zip": "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "terraform-provider-null_3.2.1_darwin_arm64.zip": "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "terraform-provider-null_3.2.1_linux_386.zip": "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "terraform-provider-null_3.2.1_freebsd_386.zip": "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "terraform-provider-null_3.2.1_linux_arm64.zip": "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + }, + ), + } + + cases := []struct { + desc string + src string + lockfile string + want string + ok bool + }{ + { + desc: "simple", + src: ` +terraform { + required_version = "1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + source = "hashicorp/null" + version = "3.2.1" + } + + github = { + source = "integrations/github" + version = "4.28.0" + } + } +} +`, + lockfile: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = "5.4.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", + "h1:Pctug/s/2Hg5FJqjYcTM0kPyx3AoYK1MpRWO0T9V2ns=", + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = "5.4.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + ok: true, + }, + { + desc: "noop", + src: ` +terraform { + required_version = "1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + source = "hashicorp/null" + version = "3.2.1" + } + + github = { + source = "integrations/github" + version = "4.28.0" + } + } +} +`, + lockfile: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = "5.4.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = "5.4.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + ok: true, + }, + { + desc: "update multiple providers", + src: ` +terraform { + required_version = "1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + source = "hashicorp/null" + version = "3.2.1" + } + + github = { + source = "integrations/github" + version = "4.28.0" + } + } +} +`, + lockfile: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.3.0" + constraints = "5.3.0" + hashes = [ + "h1:3KbfDs6Sd6pDlVTB9wYCxndEWNmTlShb/rBHnr4/+OE=", + "h1:89Ara9HnoQzGsFK1nU0fPD8h0SsHJnlVc8mUfOQSAYE=", + "h1:HKlxtbkaT/YKPtzJLGm8np5JiaPSys/aG6Lbj8QB/5c=", + "zh:001814dcf6b2329de5e2c9223c4f1e95a0f60d6670046015419053b03b3c0712", + "zh:3c511a91f53076c3a1117526bee0880b339261f1eb3feecd7854771bfef7890d", + "zh:3e6c19e048f06051c9296c7a3236946f37431ce0d84f843585c5f3e8504759d3", + "zh:476a3d918782a479166f33418192b522698e39702e8a0aec823682d3ee3082f1", + "zh:5dd0d3bff7a7acabeed600dfbbef797e189c4877f65e4b4ed572cb33e454f602", + "zh:6627f95a41e30c01b7f7c9e3db1cccba056c5257c36cccfaa0898d526211add2", + "zh:663023a4244cf7f7df2b08ab204922f7902eefe9a7b51a2c2def1a7dafe6f55f", + "zh:79cb8a22a131b7d2beb331d8443207eed10fdb4b09655048960bd5d59c8bbf3a", + "zh:8c2275a0954042cfc44843a6045543744e08bd8cad487f0bc9162cf92a9bcdcc", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ad08ae20b9402461af863772a9e4ff5677e14f3fc86d5b148bd4faaaa361f601", + "zh:b8b7bd15fc1842aeedc2e5eab03b8357cdb2b9fe3e67dd82ae240be3081bf637", + "zh:bdb3858c4c632aad8d5c4bff063f3afb18de51cec3167b3496d5bc5856915301", + "zh:f354a433ec8095b06c2701725411ffb73a20ef9b1aa325434e1bb575b5c86d52", + "zh:f47e1342883d599f4675dcfdeb9707cdfcfaf53c677f93fd5c410580d4dece13", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", + "h1:Pctug/s/2Hg5FJqjYcTM0kPyx3AoYK1MpRWO0T9V2ns=", + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = "5.4.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + ok: true, + }, + { + desc: "create missing providers", + src: ` +terraform { + required_version = "1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.4.0" + } + + null = { + source = "hashicorp/null" + version = "3.2.1" + } + + github = { + source = "integrations/github" + version = "4.28.0" + } + } +} +`, + lockfile: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = "5.4.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} +`, + ok: true, + }, + { + desc: "create from empty", + src: ` +terraform { + required_version = "1.5.0" + + required_providers { + null = { + source = "hashicorp/null" + version = "3.2.1" + } + } +} +`, + lockfile: ``, + want: ` +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} +`, + ok: true, + }, + { + desc: "ignore unsupported constaints", + src: ` +terraform { + required_version = "1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.3.0" + } + + null = { + source = "hashicorp/null" + version = "3.2.1" + } + + github = { + source = "integrations/github" + version = "4.28.0" + } + } +} +`, + lockfile: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = ">= 5.3.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", + "h1:Pctug/s/2Hg5FJqjYcTM0kPyx3AoYK1MpRWO0T9V2ns=", + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.4.0" + constraints = ">= 5.3.0" + hashes = [ + "h1:4eGsUS3r5eApQc19t8woc6d+sQLaOBaCSaK5GyGcWf0=", + "h1:Jol4lNIzMrREQzUBSveCLX0iQLy7dm0OF+IYY2GKrhY=", + "h1:ny1YPz2LiHTasDVNh6/HEvh1c9+TN/ftgAHh84bmy1E=", + "zh:1db5f81089216831bb0fdff9ddc3772efa133397c66ec276bc75b96eec06e23f", + "zh:26fe5fdf399192b5724d21854fbec650c158f8ee9eb1dc52a50f7da0f2bc07ac", + "zh:2946d9e333b1efe01588ee9f9771169fd3c3a4a7cb78ed8f91e8b3efd1a73850", + "zh:36ed69e8d3029332c8a52a70940f714fd579b9fd95f5569cc010ef11162f5bf7", + "zh:46ba5ad1c3a3ef98c346356cfa4bdd9c2501c661c2513bb92f4413f2482fb24b", + "zh:46c10aaa9672b54a14b0e0effdd6ecd9b8a539b3bfe273ac54111e7352a7bb4b", + "zh:47d7f57bcbe4fba2f960ab6c4228c5e9e586be2f233a8baa8962b51a63337179", + "zh:47e41c198439ba1c4d933f808b6f47e518f8f0aae25ca42abcac97f149121e90", + "zh:526c5834de71654ee14039cb973322bf5032cb684a2a113b48fb48a0584f46f3", + "zh:6169316517b95677819ba2904dcea204fb9b55e868348e906af9164104fe7198", + "zh:7c063ef2b8d69a8db7e8bf0dcd45793ede22b259b30464ed114d330df304cdbb", + "zh:87c4f2faca636715a08be3121d26b3354415401eab89349077ca9436a0822c23", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b184b8a268f45258edd27d389ca793708f1bc3ee4d6706d154a45e93deaddde1", + "zh:ba1a998cbf4b639fa3e04b9069f0f5a289662457940726a8a51c81df400aa852", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:FbGfc+muBsC17Ohy5g806iuI1hQc4SIexpYCrQHQd8w=", + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "4.28.0" + constraints = "4.28.0" + hashes = [ + "h1:GMp4fa/6ZxeM8c8O20rZ2jpXXkBqK09oMBtWQ1WwPCo=", + "h1:PRj9EXEvLgKTmQHKUtzIG28goXJX74aRt0b/4JH6qN8=", + "h1:vAZrilSL9rq6bXb97dl06QRohtcc0btFQzFF5dHinI0=", + "zh:125a1decda8a9d4c6d18010f3c66943c868da9e984298c0e2f9dfd240ec660ec", + "zh:23a4cb334a2fbead38264f434c81e52cb52fb115cbc39537fefc9c22aaecdf35", + "zh:3cf793b1d0bc30a703315c6ecb6bb2f36d14ed310dec7e300ae4a4a3a470aafe", + "zh:47cb06845730df19256882272690221db8314199a34012ac7e690e0550ca9404", + "zh:5d6e76624d60b6298ee47c10cc262adc9f361f4648f40faf81ee3a8d6beaad31", + "zh:6415a5c6ba5b28f1f410845706cff0390718113f7d987aaa011553b041ba2005", + "zh:70ce96d7aa424aef47d4b049d39aff036ae6377dacd5c077501eb0f353901cc6", + "zh:9803fc59cf71ea629308773d429c9ca00985acdcc02d9755fc59900bcf6d1d00", + "zh:a9a505f208f569ee44a0a6a7c975e3441bb8d61dbf9831c44c3be299e2cf1a21", + "zh:a9d9a17b0618ea14f9fa49dfc1329b01473a9d708011fca32cd01b474051d169", + "zh:bce0257085a5d6c9f0e6cdd5a704c50286c5382f840384a2a50c69d8488652bf", + "zh:d7272bb396e67ff22d7f4628d152fa66610cf7507a4e63d72ef50fde651e39bf", + "zh:e2aab496c17acb8c2bdd5af9e830e9f91f869d9fc173e6dd65b7475e8baa6f82", + "zh:ea20984a5386fc4a6856eed58d261c5124fc8ca72bc6ee142c1092036a3c8360", + ] +} +`, + ok: true, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + fs := afero.NewMemMapFs() + dirname := "test" + err := fs.MkdirAll(dirname, os.ModePerm) + if err != nil { + t.Fatalf("failed to create dir: %s", err) + } + + err = afero.WriteFile(fs, filepath.Join(dirname, "main.tf"), []byte(tc.src), 0644) + if err != nil { + t.Fatalf("failed to write file: %s", err) + } + + err = afero.WriteFile(fs, filepath.Join(dirname, ".terraform.lock.hcl"), []byte(tc.lockfile), 0644) + if err != nil { + t.Fatalf("failed to write file: %s", err) + } + + o := Option{ + updateType: "lock", + platforms: platforms, + } + gc, err := NewGlobalContext(fs, o) + if err != nil { + t.Fatalf("failed to new global context: %s", err) + } + + index := lock.NewMockIndex(pvs) + u, err := NewLockUpdater(platforms, index) + if err != nil { + t.Fatalf("failed to new LockUpdater: %s", err) + } + + f, diags := hclwrite.ParseConfig([]byte(tc.lockfile), ".terraform.lock.hcl", hcl.Pos{Line: 1, Column: 1}) + if diags.HasErrors() { + t.Fatalf("unexpected diagnostics: %s", diags) + } + + mc, err := NewModuleContext(dirname, gc) + if err != nil { + t.Fatalf("failed to new module context: %s", err) + } + + err = u.Update(context.Background(), mc, ".terraform.lock.hcl", f) + if tc.ok && err != nil { + t.Errorf("faild to call Update: err = %s", err) + } + + got := string(hclwrite.Format(f.BuildTokens(nil).Bytes())) + + if !tc.ok && err == nil { + t.Errorf("expect to fail, but success: got = %s", got) + } + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", got, tc.want, diff) + } + }) + } +} diff --git a/tfupdate/module.go b/tfupdate/module.go index 9877ee8..c64bb4d 100644 --- a/tfupdate/module.go +++ b/tfupdate/module.go @@ -1,6 +1,8 @@ package tfupdate import ( + "context" + "path/filepath" "regexp" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -40,7 +42,12 @@ func NewModuleUpdater(name string, version string) (Updater, error) { // Update updates the module version constraint. // Note that this method will rewrite the AST passed as an argument. -func (u *ModuleUpdater) Update(f *hclwrite.File) error { +func (u *ModuleUpdater) Update(_ context.Context, _ *ModuleContext, filename string, f *hclwrite.File) error { + if filepath.Ext(filename) != ".tf" { + // skip a file without .tf extension. + return nil + } + return u.updateModuleBlock(f) } diff --git a/tfupdate/module_test.go b/tfupdate/module_test.go index 80088f6..e6e6d0a 100644 --- a/tfupdate/module_test.go +++ b/tfupdate/module_test.go @@ -1,6 +1,7 @@ package tfupdate import ( + "context" "reflect" "testing" @@ -56,13 +57,15 @@ func TestNewModuleUpdater(t *testing.T) { func TestUpdateModule(t *testing.T) { cases := []struct { - src string - name string - version string - want string - ok bool + filename string + src string + name string + version string + want string + ok bool }{ { + filename: "main.tf", src: ` module "vpc" { source = "terraform-aws-modules/vpc/aws" @@ -80,6 +83,7 @@ module "vpc" { ok: true, }, { + filename: "main.tf", src: ` module "vpc1" { source = "terraform-aws-modules/vpc/aws" @@ -105,6 +109,7 @@ module "vpc2" { ok: true, }, { + filename: "main.tf", src: ` module "vpc" { source = "terraform-aws-modules/vpc/aws" @@ -122,6 +127,7 @@ module "vpc" { ok: true, }, { + filename: "main.tf", src: ` module "vpc" { source = "terraform-aws-modules/vpc/aws" @@ -137,6 +143,7 @@ module "vpc" { ok: true, }, { + filename: "main.tf", src: ` module "vpc" { source = "git::https://example.com/vpc.git?ref=v1.2.0" @@ -148,6 +155,40 @@ module "vpc" { module "vpc" { source = "git::https://example.com/vpc.git?ref=v1.3.0" } +`, + ok: true, + }, + { + filename: ".terraform.lock.hcl", + src: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + ] +} + +`, + name: "git::https://example.com/vpc.git", + version: "1.3.0", + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + ] +} + `, ok: true, }, @@ -158,12 +199,12 @@ module "vpc" { name: tc.name, version: tc.version, } - f, diags := hclwrite.ParseConfig([]byte(tc.src), "", hcl.Pos{Line: 1, Column: 1}) + f, diags := hclwrite.ParseConfig([]byte(tc.src), tc.filename, hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatalf("unexpected diagnostics: %s", diags) } - err := u.Update(f) + err := u.Update(context.Background(), nil, tc.filename, f) if tc.ok && err != nil { t.Errorf("Update() with src = %s, name = %s, version = %s returns unexpected err: %+v", tc.src, tc.name, tc.version, err) } diff --git a/tfupdate/option.go b/tfupdate/option.go index 90dffd9..cf36c6f 100644 --- a/tfupdate/option.go +++ b/tfupdate/option.go @@ -7,16 +7,25 @@ import ( // Option is a set of parameters to update. type Option struct { - // A type of updater. Valid value is terraform or provider. + // A type of updater. Valid values are as follows: + // - terraform + // - provider + // - module + // - lock updateType string // If an updateType is terraform, there is no meaning. - // If an updateType is provider, Set a name of provider. + // If an updateType is provider or module, Set a name of provider or module. name string // a new version constraint version string + // platforms is a list of target platforms to generate hash values. + // Target platform names consist of an operating system and a CPU + // architecture such as darwin_arm64. + platforms []string + // If a recursive flag is true, it checks and updates directories recursively. recursive bool @@ -25,7 +34,7 @@ type Option struct { } // NewOption returns an option. -func NewOption(updateType string, name string, version string, recursive bool, ignorePaths []string) (Option, error) { +func NewOption(updateType string, name string, version string, platforms []string, recursive bool, ignorePaths []string) (Option, error) { regexps := make([]*regexp.Regexp, 0, len(ignorePaths)) for _, ignorePath := range ignorePaths { if len(ignorePath) == 0 { @@ -43,6 +52,7 @@ func NewOption(updateType string, name string, version string, recursive bool, i updateType: updateType, name: name, version: version, + platforms: platforms, recursive: recursive, ignorePaths: regexps, }, nil diff --git a/tfupdate/option_test.go b/tfupdate/option_test.go index c85e7e5..e2ba364 100644 --- a/tfupdate/option_test.go +++ b/tfupdate/option_test.go @@ -11,6 +11,7 @@ func TestNewOption(t *testing.T) { updateType string name string version string + platforms []string recursive bool ignorePaths []string want Option @@ -19,11 +20,13 @@ func TestNewOption(t *testing.T) { { updateType: "terraform", version: "0.12.7", + platforms: []string{}, recursive: true, ignorePaths: []string{}, want: Option{ updateType: "terraform", version: "0.12.7", + platforms: []string{}, recursive: true, ignorePaths: []*regexp.Regexp{}, }, @@ -33,12 +36,14 @@ func TestNewOption(t *testing.T) { updateType: "provider", name: "aws", version: "2.23.0", + platforms: []string{}, recursive: true, ignorePaths: []string{}, want: Option{ updateType: "provider", name: "aws", version: "2.23.0", + platforms: []string{}, recursive: true, ignorePaths: []*regexp.Regexp{}, }, @@ -47,11 +52,13 @@ func TestNewOption(t *testing.T) { { updateType: "terraform", version: "0.12.7", + platforms: []string{}, recursive: true, ignorePaths: []string{"hoge", "fuga"}, want: Option{ updateType: "terraform", version: "0.12.7", + platforms: []string{}, recursive: true, ignorePaths: []*regexp.Regexp{regexp.MustCompile("hoge"), regexp.MustCompile("fuga")}, }, @@ -60,11 +67,13 @@ func TestNewOption(t *testing.T) { { updateType: "terraform", version: "0.12.7", + platforms: []string{}, recursive: true, ignorePaths: []string{""}, want: Option{ updateType: "terraform", version: "0.12.7", + platforms: []string{}, recursive: true, ignorePaths: []*regexp.Regexp{}, }, @@ -73,25 +82,41 @@ func TestNewOption(t *testing.T) { { updateType: "terraform", version: "0.12.7", + platforms: []string{}, recursive: true, ignorePaths: []string{`\`}, want: Option{}, ok: false, }, + { + updateType: "lock", + version: "", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + recursive: true, + ignorePaths: []string{}, + want: Option{ + updateType: "lock", + version: "", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + recursive: true, + ignorePaths: []*regexp.Regexp{}, + }, + ok: true, + }, } for _, tc := range cases { - got, err := NewOption(tc.updateType, tc.name, tc.version, tc.recursive, tc.ignorePaths) + got, err := NewOption(tc.updateType, tc.name, tc.version, tc.platforms, tc.recursive, tc.ignorePaths) if tc.ok && err != nil { - t.Errorf("NewOption() with updateType = %s, name = %s, version = %s, recursive = %t, ignorePath = %#v returns unexpected err: %+v", tc.updateType, tc.name, tc.version, tc.recursive, tc.ignorePaths, err) + t.Errorf("NewOption() with updateType = %s, name = %s, version = %s, platforms = %#v, recursive = %t, ignorePath = %#v returns unexpected err: %+v", tc.updateType, tc.name, tc.version, tc.platforms, tc.recursive, tc.ignorePaths, err) } if !tc.ok && err == nil { - t.Errorf("NewOption() with updateType = %s, name = %s, version = %s, recursive = %t, ignorePath = %#v expects to return an error, but no error", tc.updateType, tc.name, tc.version, tc.recursive, tc.ignorePaths) + t.Errorf("NewOption() with updateType = %s, name = %s, version = %s, platforms = %#v, recursive = %t, ignorePath = %#v expects to return an error, but no error", tc.updateType, tc.name, tc.version, tc.platforms, tc.recursive, tc.ignorePaths) } if !reflect.DeepEqual(got, tc.want) { - t.Errorf("NewOption() with updateType = %s, name = %s, version = %s, recursive = %t, ignorePath = %#v returns %#v, but want = %#v", tc.updateType, tc.name, tc.version, tc.recursive, tc.ignorePaths, got, tc.want) + t.Errorf("NewOption() with updateType = %s, name = %s, version = %s, platforms = %#v, recursive = %t, ignorePath = %#v returns %#v, but want = %#v", tc.updateType, tc.name, tc.version, tc.platforms, tc.recursive, tc.ignorePaths, got, tc.want) } } } diff --git a/tfupdate/provider.go b/tfupdate/provider.go index 3312bdc..4407b69 100644 --- a/tfupdate/provider.go +++ b/tfupdate/provider.go @@ -1,7 +1,9 @@ package tfupdate import ( + "context" "fmt" + "path/filepath" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -34,7 +36,12 @@ func NewProviderUpdater(name string, version string) (Updater, error) { // Update updates the provider version constraint. // Note that this method will rewrite the AST passed as an argument. -func (u *ProviderUpdater) Update(f *hclwrite.File) error { +func (u *ProviderUpdater) Update(_ context.Context, _ *ModuleContext, filename string, f *hclwrite.File) error { + if filepath.Ext(filename) != ".tf" { + // skip a file without .tf extension. + return nil + } + if err := u.updateTerraformBlock(f); err != nil { return err } diff --git a/tfupdate/provider_test.go b/tfupdate/provider_test.go index d4659f2..964aafd 100644 --- a/tfupdate/provider_test.go +++ b/tfupdate/provider_test.go @@ -1,6 +1,7 @@ package tfupdate import ( + "context" "reflect" "testing" @@ -56,13 +57,15 @@ func TestNewProviderUpdater(t *testing.T) { func TestUpdateProvider(t *testing.T) { cases := []struct { - src string - name string - version string - want string - ok bool + filename string + src string + name string + version string + want string + ok bool }{ { + filename: "main.tf", src: ` terraform { required_version = "0.12.4" @@ -84,6 +87,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` provider "aws" { version = "2.11.0" @@ -101,6 +105,7 @@ provider "aws" { ok: true, }, { + filename: "main.tf", src: ` terraform { required_version = "0.12.4" @@ -122,6 +127,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` provider "aws" { region = "ap-northeast-1" @@ -137,6 +143,7 @@ provider "aws" { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -186,6 +193,7 @@ provider "aws" { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -211,6 +219,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -234,6 +243,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -269,6 +279,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -290,6 +301,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -319,6 +331,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -344,6 +357,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -375,6 +389,40 @@ terraform { } } } +`, + ok: true, + }, + { + filename: ".terraform.lock.hcl", + src: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + ] +} + +`, + name: "null", + version: "3.2.1", + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + ] +} + `, ok: true, }, @@ -385,12 +433,12 @@ terraform { name: tc.name, version: tc.version, } - f, diags := hclwrite.ParseConfig([]byte(tc.src), "", hcl.Pos{Line: 1, Column: 1}) + f, diags := hclwrite.ParseConfig([]byte(tc.src), tc.filename, hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatalf("unexpected diagnostics: %s", diags) } - err := u.Update(f) + err := u.Update(context.Background(), nil, tc.filename, f) if tc.ok && err != nil { t.Errorf("Update() with src = %s, name = %s, version = %s returns unexpected err: %+v", tc.src, tc.name, tc.version, err) } diff --git a/tfupdate/terraform.go b/tfupdate/terraform.go index d8d8f80..d81d95a 100644 --- a/tfupdate/terraform.go +++ b/tfupdate/terraform.go @@ -1,6 +1,9 @@ package tfupdate import ( + "context" + "path/filepath" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/pkg/errors" "github.com/zclconf/go-cty/cty" @@ -24,7 +27,12 @@ func NewTerraformUpdater(version string) (Updater, error) { // Update updates the terraform version constraint. // Note that this method will rewrite the AST passed as an argument. -func (u *TerraformUpdater) Update(f *hclwrite.File) error { +func (u *TerraformUpdater) Update(_ context.Context, _ *ModuleContext, filename string, f *hclwrite.File) error { + if filepath.Ext(filename) != ".tf" { + // skip a file without .tf extension. + return nil + } + for _, tf := range allMatchingBlocks(f.Body(), "terraform", []string{}) { // set a version to attribute value only if the key exists if tf.Body().GetAttribute("required_version") != nil { diff --git a/tfupdate/terraform_test.go b/tfupdate/terraform_test.go index f2a5478..2ce97a6 100644 --- a/tfupdate/terraform_test.go +++ b/tfupdate/terraform_test.go @@ -1,6 +1,7 @@ package tfupdate import ( + "context" "reflect" "testing" @@ -46,12 +47,14 @@ func TestNewTerraformUpdater(t *testing.T) { func TestUpdateTerraform(t *testing.T) { cases := []struct { - src string - version string - want string - ok bool + filename string + src string + version string + want string + ok bool }{ { + filename: "main.tf", src: ` terraform { required_version = "0.12.6" @@ -66,6 +69,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` terraform { required_providers { @@ -84,6 +88,7 @@ terraform { ok: true, }, { + filename: "main.tf", src: ` provider "aws" { version = "2.11.0" @@ -100,6 +105,7 @@ provider "aws" { ok: true, }, { + filename: "main.tf", src: `terraform { backend "s3" { region = "ap-northeast-1" @@ -122,6 +128,39 @@ terraform { terraform { required_version = "0.12.7" } +`, + ok: true, + }, + { + filename: ".terraform.lock.hcl", + src: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + ] +} + +`, + version: "0.12.7", + want: ` +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + ] +} + `, ok: true, }, @@ -131,12 +170,12 @@ terraform { u := &TerraformUpdater{ version: tc.version, } - f, diags := hclwrite.ParseConfig([]byte(tc.src), "", hcl.Pos{Line: 1, Column: 1}) + f, diags := hclwrite.ParseConfig([]byte(tc.src), tc.filename, hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatalf("unexpected diagnostics: %s", diags) } - err := u.Update(f) + err := u.Update(context.Background(), nil, tc.filename, f) if tc.ok && err != nil { t.Errorf("Update() with src = %s, version = %s returns unexpected err: %+v", tc.src, tc.version, err) } diff --git a/tfupdate/update.go b/tfupdate/update.go index 097a9ad..725e64e 100644 --- a/tfupdate/update.go +++ b/tfupdate/update.go @@ -2,6 +2,7 @@ package tfupdate import ( "bytes" + "context" "fmt" "io" "log" @@ -9,6 +10,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/minamijoyo/tfupdate/lock" "github.com/pkg/errors" ) @@ -16,11 +18,16 @@ import ( type Updater interface { // Update updates a version constraint. // Note that this method will rewrite the AST passed as an argument. - Update(*hclwrite.File) error + Update(ctx context.Context, mc *ModuleContext, filename string, f *hclwrite.File) error } // NewUpdater is a factory method which returns an Updater implementation. func NewUpdater(o Option) (Updater, error) { + lockIndex, err := lock.NewDefaultIndex() + if err != nil { + return nil, err + } + switch o.updateType { case "terraform": return NewTerraformUpdater(o.version) @@ -28,6 +35,8 @@ func NewUpdater(o Option) (Updater, error) { return NewProviderUpdater(o.name, o.version) case "module": return NewModuleUpdater(o.name, o.version) + case "lock": + return NewLockUpdater(o.platforms, lockIndex) default: return nil, errors.Errorf("failed to new updater. unknown type: %s", o.updateType) } @@ -35,10 +44,9 @@ func NewUpdater(o Option) (Updater, error) { // UpdateHCL reads HCL from io.Reader, updates version constraints // and writes updated contents to io.Writer. -// Note that a filename is used only for an error message. // If contents changed successfully, it returns true, or otherwise returns false. // If an error occurs, Nothing is written to the output stream. -func UpdateHCL(r io.Reader, w io.Writer, filename string, o Option) (bool, error) { +func UpdateHCL(ctx context.Context, mc *ModuleContext, r io.Reader, w io.Writer, filename string) (bool, error) { input, err := io.ReadAll(r) if err != nil { return false, fmt.Errorf("failed to read input: %s", err) @@ -49,12 +57,8 @@ func UpdateHCL(r io.Reader, w io.Writer, filename string, o Option) (bool, error return false, err } - u, err := NewUpdater(o) - if err != nil { - return false, err - } - - if err = u.Update(f); err != nil { + u := mc.Updater() + if err = u.Update(ctx, mc, filename, f); err != nil { return false, err } diff --git a/tfupdate/update_test.go b/tfupdate/update_test.go index 2a4a37a..597b45b 100644 --- a/tfupdate/update_test.go +++ b/tfupdate/update_test.go @@ -2,8 +2,14 @@ package tfupdate import ( "bytes" - "reflect" + "context" "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/minamijoyo/tfupdate/lock" + "github.com/spf13/afero" ) func TestNewUpdater(t *testing.T) { @@ -46,6 +52,16 @@ func TestNewUpdater(t *testing.T) { }, ok: true, }, + { + o: Option{ + updateType: "lock", + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + }, + want: &LockUpdater{ + platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}, + }, + ok: true, + }, { o: Option{ updateType: "hoge", @@ -66,8 +82,15 @@ func TestNewUpdater(t *testing.T) { t.Errorf("NewUpdater() with o = %#v expects to return an error, but no error", tc.o) } - if !reflect.DeepEqual(got, tc.want) { - t.Errorf("NewUpdater() with o = %#v returns %#v, but want = %#v", tc.o, got, tc.want) + opts := []cmp.Option{ + cmp.AllowUnexported(TerraformUpdater{}), + cmp.AllowUnexported(ProviderUpdater{}), + cmp.AllowUnexported(ModuleUpdater{}), + cmp.AllowUnexported(LockUpdater{}), + cmpopts.IgnoreInterfaces(struct{ lock.Index }{}), + } + if diff := cmp.Diff(got, tc.want, opts...); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.want), diff) } } } @@ -172,26 +195,24 @@ provider "invalid" { isUpdated: false, ok: true, }, - { - src: ` -provider "aws" { - version = "2.11.0" -} -`, - o: Option{ - updateType: "hoge", - version: "0.0.1", - }, - want: "", - isUpdated: false, - ok: false, - }, } for _, tc := range cases { r := bytes.NewBufferString(tc.src) w := &bytes.Buffer{} - isUpdated, err := UpdateHCL(r, w, "test", tc.o) + + fs := afero.NewMemMapFs() + gc, err := NewGlobalContext(fs, tc.o) + if err != nil { + t.Fatalf("failed to new global context: %s", err) + } + + mc, err := NewModuleContext(".", gc) + if err != nil { + t.Fatalf("failed to new module context: %s", err) + } + + isUpdated, err := UpdateHCL(context.Background(), mc, r, w, "main.tf") if tc.ok && err != nil { t.Errorf("UpdateHCL() with src = %s, o = %#v returns unexpected err: %+v", tc.src, tc.o, err) }