File tfupdate-0.9.2.obscpio of Package tfupdate
07070100000000000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001700000000tfupdate-0.9.2/.claude07070100000001000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000002000000000tfupdate-0.9.2/.claude/projects07070100000002000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000003600000000tfupdate-0.9.2/.claude/projects/ai_agent_optimization07070100000003000081A400000000000000000000000168975E3600001261000000000000000000000000000000000000003E00000000tfupdate-0.9.2/.claude/projects/ai_agent_optimization/TODO.md# AI Agent Optimization TODO
This file manages improvement tasks to optimize the tfupdate project for AI agent coding efficiency.
## Progress Overview
- [ ] Phase 1: Foundation Strengthening (3 items)
- [ ] Phase 2: Developer Experience Enhancement (3 items)
- [ ] Phase 3: Automation & Monitoring (2 items)
- [ ] Phase 4: AI Optimization (2 items)
---
## Phase 1: Foundation Strengthening (Highest Priority)
### 1. Test Coverage Improvement
- [ ] Improve `main.go` testability
- [ ] Separate logic into testable structures
- [ ] Utilize existing UI/FS abstractions for mocking
- [ ] Add unit tests for `command/` package
- [ ] Test basic operations for each command
- [ ] Test error handling scenarios
- [ ] Consolidate and standardize test helper functions
### 2. Architecture Documentation Creation
- [ ] Create system design diagrams
- [ ] Package dependency relationship diagram
- [ ] Data flow diagrams
- [ ] Add interface specification documentation
- [ ] Detailed specs for main interfaces (Updater, Release, etc.)
- [ ] Formalize API contracts
- [ ] Record design decisions (ADR: Architecture Decision Records)
- [ ] Why interface-driven design was adopted
- [ ] Rationale for factory pattern adoption
### 3. Code Readability Enhancement
- [ ] Split lengthy functions
- [ ] Identify and split functions exceeding 100 lines
- [ ] Apply single responsibility principle
- [ ] Rename functions/variables to express intent
- [ ] Eliminate abbreviations
- [ ] Use names that express business logic
- [ ] Add comments to complex logic
- [ ] Explain algorithms
- [ ] Clarify non-obvious processing intentions
---
## Phase 2: Developer Experience Enhancement (High Priority)
### 4. Strengthen Linting & Static Analysis
- [ ] Add golangci-lint rules
- [ ] gocritic (code quality checks)
- [ ] gocyclo (cyclomatic complexity)
- [ ] dupl (duplicate code detection)
- [ ] Enable security rules
- [ ] Strengthen gosec configuration
- [ ] Add additional security checkers
- [ ] Add custom rules
- [ ] Project-specific naming conventions
- [ ] Enforce interface usage
### 5. Standardize Development Environment
- [ ] Improve development Docker container
- [ ] Complete setup of necessary tools
- [ ] Optimize for development efficiency
- [ ] Add editor configuration files
- [ ] VSCode settings (.vscode/)
- [ ] GoLand settings (.idea/)
- [ ] Standardize debug configuration
- [ ] launch.json for VSCode
- [ ] Standardize Delve configuration
### 6. Improve Error Handling & Logging
- [ ] Introduce structured logging
- [ ] JSON format log output option
- [ ] Refine log levels
- [ ] Improve error message consistency
- [ ] Unify error classification
- [ ] User-friendly messages
- [ ] Enhance debug information
- [ ] Add trace information
- [ ] Record execution context
---
## Phase 3: Automation & Monitoring (Medium Priority)
### 7. Strengthen CI/CD Pipeline
- [ ] Dependency vulnerability scanning
- [ ] GitHub Security Advisories integration
- [ ] Automatic PR creation for vulnerability fixes
- [ ] Automate performance testing
- [ ] Add benchmark tests
- [ ] Detect performance regression
- [ ] Improve release automation
- [ ] Automate semantic versioning
- [ ] Auto-generate CHANGELOG
### 8. Metrics & Monitoring
- [ ] Collect code metrics
- [ ] Continuous monitoring of complexity and duplication
- [ ] Set quality gates
- [ ] Monitor test execution time
- [ ] Identify slow tests
- [ ] Performance improvements
- [ ] Optimize build time
- [ ] Optimize dependencies
- [ ] Improve parallelization
---
## Phase 4: AI Optimization (Future Investment)
### 9. Code Generation Support
- [ ] Template & scaffolding tools
- [ ] Generate new Updater templates
- [ ] Auto-generate test files
- [ ] Automatic boilerplate code generation
- [ ] Generate interface implementations
- [ ] Auto-update mock implementations
- [ ] Refactoring support tools
- [ ] Safe renaming
- [ ] Automatic structure adjustments
### 10. Documentation Automation
- [ ] Auto-generate API documentation from code
- [ ] Enhance godoc
- [ ] Generate OpenAPI specifications
- [ ] Automatic change history recording
- [ ] Git-based change tracking
- [ ] Automatic impact analysis
- [ ] Auto-update technical specifications
- [ ] Automatic architecture diagram updates
- [ ] Auto-generate dependency diagrams
---
## Guidelines
- Progress each item incrementally
- Always write tests first (TDD)
- Minimize impact on existing functionality
- Update documentation simultaneously with implementation
- Always consider AI agent work efficiency
- **ALL documentation and comments must be written in English**07070100000004000081A400000000000000000000000168975E3600000017000000000000000000000000000000000000001D00000000tfupdate-0.9.2/.dockerignore.envrc
bin/
tmp/
dist/
07070100000005000081A400000000000000000000000168975E360000001F000000000000000000000000000000000000001D00000000tfupdate-0.9.2/.envrc.sampleexport TERRAFORM_VERSION=1.5.2
07070100000006000081A400000000000000000000000168975E3600000005000000000000000000000000000000000000001B00000000tfupdate-0.9.2/.go-version1.24
07070100000007000081A400000000000000000000000168975E36000000CF000000000000000000000000000000000000001D00000000tfupdate-0.9.2/.golangci.yml# https://golangci-lint.run/usage/configuration/
linters:
disable-all: true
enable:
- errcheck
- goimports
- gosec
- gosimple
- govet
- ineffassign
- revive
- staticcheck
07070100000008000081A400000000000000000000000168975E360000031E000000000000000000000000000000000000001F00000000tfupdate-0.9.2/.goreleaser.ymlversion: 2
builds:
- binary: tfupdate
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm64
env:
- CGO_ENABLED=0
release:
prerelease: auto
changelog:
filters:
exclude:
- Merge pull request
- Merge branch
- Update README
- Update CHANGELOG
brews:
- repository:
owner: minamijoyo
name: homebrew-tfupdate
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
commit_author:
name: "Masayuki Morita"
email: minamijoyo@gmail.com
homepage: https://github.com/minamijoyo/tfupdate
description: "Update version constraints in your Terraform / OpenTofu configurations"
skip_upload: auto
test: |
system "#{bin}/tfupdate --version"
install: |
bin.install "tfupdate"
07070100000009000081A400000000000000000000000168975E3600002F4D000000000000000000000000000000000000001C00000000tfupdate-0.9.2/CHANGELOG.md## master (Unreleased)
## 0.9.2 (2025/08/09)
ENHANCEMENTS:
* Update golang.org/x/crypto to v0.41.0 ([#143](https://github.com/minamijoyo/tfupdate/pull/143))
* Update hcl to v2.24.0 ([#144](https://github.com/minamijoyo/tfupdate/pull/144))
* Add support for Terraform 1.12 ([#145](https://github.com/minamijoyo/tfupdate/pull/145))
* Add support for OpenTofu 1.10 ([#146](https://github.com/minamijoyo/tfupdate/pull/146))
## 0.9.1 (2025/05/05)
BUG FIXES:
* Fix the build issue with the replace directive ([#136](https://github.com/minamijoyo/tfupdate/pull/136))
## 0.9.0 (2025/05/03)
NEW FEATURES:
The tfupdate now supports OpenTofu, a community fork of Terraform.
If you want to use the public OpenTofu registry, set the `TFREGISTRY_BASE_URL` environment variable to `https://registry.opentofu.org/`.
```
$ export TFREGISTRY_BASE_URL=https://registry.opentofu.org/
```
* Add support for updating version constraints of opentofu core ([#127](https://github.com/minamijoyo/tfupdate/pull/127))
* Add support for .tofu extension ([#128](https://github.com/minamijoyo/tfupdate/pull/128))
* Add support for the OpenTofu registry as a release source ([#130](https://github.com/minamijoyo/tfupdate/pull/130))
* Allow TFREGISTRY_BASE_URL to set the host of the Terraform registry ([#132](https://github.com/minamijoyo/tfupdate/pull/132))
* Add support for updating .terraform.lock.hcl using OpenTofu registry ([#134](https://github.com/minamijoyo/tfupdate/pull/134))
ENHANCEMENTS:
* Update Go to v1.24 ([#124](https://github.com/minamijoyo/tfupdate/pull/124))
* Update hcl to v2.23.0 ([#125](https://github.com/minamijoyo/tfupdate/pull/125))
* Add support for Terraform 1.11 ([#126](https://github.com/minamijoyo/tfupdate/pull/126))
* Pin all GitHub Actions ([#129](https://github.com/minamijoyo/tfupdate/pull/129))
* Unify tfregistry config for release and lock packages ([#133](https://github.com/minamijoyo/tfupdate/pull/133))
NOTE:
This release contains breaking changes as Go packages, but as a CLI, it should not affect end users.
## 0.8.5 (2024/08/02)
ENHANCEMENTS:
* Update goreleaser to v2 ([#121](https://github.com/minamijoyo/tfupdate/pull/121))
* Switch to the official action for creating GitHub App token ([#122](https://github.com/minamijoyo/tfupdate/pull/122))
## 0.8.4 (2024/08/01)
ENHANCEMENTS:
* Pin goreleaser to v1 ([#119](https://github.com/minamijoyo/tfupdate/pull/119))
## 0.8.3 (2024/08/01)
ENHANCEMENTS:
* Update hcl to v2.21.0 ([#114](https://github.com/minamijoyo/tfupdate/pull/114))
* Use docker compose command instead of docker-compose ([#115](https://github.com/minamijoyo/tfupdate/pull/115))
* Add support for Terraform 1.9 ([#116](https://github.com/minamijoyo/tfupdate/pull/116))
* Update alpine to v3.20 ([#117](https://github.com/minamijoyo/tfupdate/pull/117))
* Update golangci lint to v1.59.1 ([#118](https://github.com/minamijoyo/tfupdate/pull/118))
## 0.8.2 (2024/04/15)
ENHANCEMENTS:
* feat: update to use go 1.22 ([#111](https://github.com/minamijoyo/tfupdate/pull/111))
* Add support for provider-defined functions ([#112](https://github.com/minamijoyo/tfupdate/pull/112))
* Add support for Terraform 1.8 ([#113](https://github.com/minamijoyo/tfupdate/pull/113))
## 0.8.1 (2024/01/26)
NEW FEATURES:
* tfupdate module command support regex matches ([#108](https://github.com/minamijoyo/tfupdate/pull/108))
ENHANCEMENTS:
* Update hcl to v2.19.1 ([#109](https://github.com/minamijoyo/tfupdate/pull/109))
* Add support for Terraform 1.7 ([#110](https://github.com/minamijoyo/tfupdate/pull/110))
## 0.8.0 (2023/10/09)
NOTE:
Starting from v0.8.0, the tfupdate provider command now supports namespaces. To maintain backward compatibility, the left-hand key in the required_providers block is still evaluated as the short name if the namespace is omitted. Still, if the provider name contains /, we assume that users intend to use namespaces and check the source address.
BREAKING CHANGES:
While backward compatibility is maintained for most use cases, some partner providers that relied on the legacy terraform-providers/ org redirects must explicitly specify their namespace. Specifically, namespaces must be explicit if all the following conditions are met:
- The tfupdate provider command does not explicitly specify the namespace.
- The tfupdate provider command does not explicitly specify the version.
- Redirected from legacy terraform-providers/ org to partner org on GitHub.
- Not hosted under hashicorp/ org on GitHub.
- Not redirected from hashicorp/ org to partner org on GitHub.
NEW FEATURES:
* Add support for provider namespace ([#102](https://github.com/minamijoyo/tfupdate/pull/102))
BUG FIXES:
* Fixed a crash when parsing invalid release versions as SemVer ([#103](https://github.com/minamijoyo/tfupdate/pull/103))
ENHANCEMENTS:
* deps: update to use go1.21 ([#98](https://github.com/minamijoyo/tfupdate/pull/98))
* Update actions/checkout to v4 ([#100](https://github.com/minamijoyo/tfupdate/pull/100))
* Update hcl to v2.18.1 ([#101](https://github.com/minamijoyo/tfupdate/pull/101))
* Add support for Terraform v1.6 ([#104](https://github.com/minamijoyo/tfupdate/pull/104))
## 0.7.2 (2023/07/07)
BUG FIXES:
* Fix a regression issue for updating .hcl file ([#97](https://github.com/minamijoyo/tfupdate/pull/97))
## 0.7.1 (2023/07/05)
BUG FIXES:
* Fix a regression issue for using absolute path ([#94](https://github.com/minamijoyo/tfupdate/pull/94))
ENHANCEMENTS:
* Set docker build timeout to 20m ([#91](https://github.com/minamijoyo/tfupdate/pull/91))
* Update docker related actions to latest ([#92](https://github.com/minamijoyo/tfupdate/pull/92))
## 0.7.0 (2023/07/04)
NEW FEATURES:
* Add native support for updating .terraform.lock.hcl ([#90](https://github.com/minamijoyo/tfupdate/pull/90))
## 0.6.8 (2023/06/06)
ENHANCEMENTS:
* Update Go to v1.20 and Alpine 3.18 ([#87](https://github.com/minamijoyo/tfupdate/pull/87))
* Update hcl to v2.17.0 ([#88](https://github.com/minamijoyo/tfupdate/pull/88))
* Set docker build timeout to 10m ([#89](https://github.com/minamijoyo/tfupdate/pull/89))
## 0.6.7 (2022/08/29)
ENHANCEMENTS:
* deps: update to use go1.19 ([#74](https://github.com/minamijoyo/tfupdate/pull/74))
* Add jq, openssl, and curl to Docker image ([#76](https://github.com/minamijoyo/tfupdate/pull/76))
## 0.6.6 (2022/08/10)
ENHANCEMENTS:
* Update golangci-lint to v1.45.2 and actions to latest ([#71](https://github.com/minamijoyo/tfupdate/pull/71))
* Use GitHub App token for updating brew formula on release ([#73](https://github.com/minamijoyo/tfupdate/pull/73))
## 0.6.5 (2022/03/24)
BUG FIXES:
* Fix go install error ([#70](https://github.com/minamijoyo/tfupdate/pull/70))
## 0.6.4 (2022/01/13)
ENHANCEMENTS:
* Use golangci-lint instead of golint ([#60](https://github.com/minamijoyo/tfupdate/pull/60))
* Fix lint errors ([#61](https://github.com/minamijoyo/tfupdate/pull/61))
* Add support for linux/arm64 Docker image ([#62](https://github.com/minamijoyo/tfupdate/pull/62))
## 0.6.3 (2021/11/12)
ENHANCEMENTS:
* Update Go to v1.17.3 and Alpine to 3.14 ([#56](https://github.com/minamijoyo/tfupdate/pull/56))
* Update hcl to v2.10.1 ([#57](https://github.com/minamijoyo/tfupdate/pull/57))
* Add arm64 builds to support M1 mac ([#58](https://github.com/minamijoyo/tfupdate/pull/58))
## 0.6.2 (2021/10/25)
BUG FIXES:
* Fix panic when version key is quoted ([#52](https://github.com/minamijoyo/tfupdate/pull/52))
ENHANCEMENTS:
* Restrict permissions for GitHub Actions ([#53](https://github.com/minamijoyo/tfupdate/pull/53))
* Set timeout for GitHub Actions ([#54](https://github.com/minamijoyo/tfupdate/pull/54))
## 0.6.1 (2021/07/19)
BUG FIXES:
* Fix goreleaser settings for brew ([#50](https://github.com/minamijoyo/tfupdate/pull/50))
## 0.6.0 (2021/07/19)
BREAKING CHANGES:
* Build & push docker images on GitHub Actions ([#49](https://github.com/minamijoyo/tfupdate/pull/49))
The `latest` tag of docker image now points at the latest release. Previously the `latest` tag pointed at the master branch, if you want to use the master branch, use the `master` tag instead.
ENHANCEMENTS:
* Drop goreleaser dependencies ([#48](https://github.com/minamijoyo/tfupdate/pull/48))
* Move CI to GitHub Actions ([#47](https://github.com/minamijoyo/tfupdate/pull/47))
## 0.5.1 (2021/05/27)
ENHANCEMENTS:
* Allow to parse the configuration_aliases syntax in Terraform v0.15 ([#43](https://github.com/minamijoyo/tfupdate/pull/43))
## 0.5.0 (2021/05/14)
BREAKING CHANGES:
* Sort releases in semver order ([#41](https://github.com/minamijoyo/tfupdate/pull/41))
* Hide pre-releases by default in the release list command ([#42](https://github.com/minamijoyo/tfupdate/pull/42))
The `release latest` command now returns the latest release in semantic versioning order. Previously it returned the most recent release. In many cases it was the same, but in some cases the most recent older patch release was returned.
The `release list` command now sorts releases in semantic versioning order and hides pre-releases. If you want to show pre-releases, use the `--pre-release` flag.
ENHANCEMENTS:
* Update Go to v1.16.3 ([#37](https://github.com/minamijoyo/tfupdate/pull/37))
* Update hcl to v2.10.0 ([#38](https://github.com/minamijoyo/tfupdate/pull/38))
* Update alpine to v3.12 ([#39](https://github.com/minamijoyo/tfupdate/pull/39))
## 0.4.3 (2020/12/10)
BUG FIXES:
* Fix unexpected broken parentheses expression ([#34](https://github.com/minamijoyo/tfupdate/pull/34))
ENHANCEMENTS:
* Prevent uploading pre-release to Homebrew ([#29](https://github.com/minamijoyo/tfupdate/pull/29))
## 0.4.2 (2020/10/01)
NEW FEATURES:
* (experimental) Support getting release versions of a provider from the terraform registry ([#26](https://github.com/minamijoyo/tfupdate/pull/26))
The release list/latest command now allows you to get the release version from Terraform Registry with `--source-type tfregistryProvider`, which is an experimental feature because we are currently depending on an undocumented Registry API. We are planning to switch another API which Terraform CLI depends on.
ENHANCEMENTS:
* Ignore case for log level passed in TFUPDATE_LOG environment variable ([#25](https://github.com/minamijoyo/tfupdate/pull/25))
## 0.4.1 (2020/07/09)
BUG FIXES:
* Use static link on build for alpine compatible ([#23](https://github.com/minamijoyo/tfupdate/pull/23))
## 0.4.0 (2020/06/18)
NEW FEATURES:
* Support a new provider source syntax in Terraform v0.13 ([#21](https://github.com/minamijoyo/tfupdate/pull/21))
ENHANCEMENTS:
* Update Go to v1.14.4 ([#20](https://github.com/minamijoyo/tfupdate/pull/20))
## 0.3.6 (2020/06/11)
BUG FIXES:
* Fix panic with legacy dot access of numeric indexes ([#19](https://github.com/minamijoyo/tfupdate/pull/19))
## 0.3.5 (2020/02/26)
NEW FEATURES:
* Add release list command ([#16](https://github.com/minamijoyo/tfupdate/pull/16))
## 0.3.4 (2020/02/13)
NEW FEATURES:
* Add support for Terraform Registry Module as a release data source ([#15](https://github.com/minamijoyo/tfupdate/pull/15))
## 0.3.3 (2020/01/09)
BUG FIXES:
* Fix a bug for parsing a map index ([#13](https://github.com/minamijoyo/tfupdate/pull/13))
## 0.3.2 (2019/12/30)
NEW FEATURES:
* Add support for GitLab projects to release latest ([#11](https://github.com/minamijoyo/tfupdate/pull/11))
## 0.3.1 (2019/12/19)
NEW FEATURES:
* Add support for GitHub private repository ([#9](https://github.com/minamijoyo/tfupdate/pull/9))
ENHANCEMENTS:
* Make release interface more flexible ([#8](https://github.com/minamijoyo/tfupdate/pull/8))
BUG FIXES:
* Fix instruction for building from source ([#6](https://github.com/minamijoyo/tfupdate/pull/6))
## 0.3.0 (2019/11/28)
NEW FEATURES:
* Add module support ([#2](https://github.com/minamijoyo/tfupdate/pull/2))
Note: Automatic latest version resolution is not currently supported for modules.
## 0.2.1 (2019/11/27)
BUG FIXES:
* Fix typo in PROVIDER_NAME argument documentation ([#1](https://github.com/minamijoyo/tfupdate/pull/1))
## 0.2.0 (2019/11/09)
Initial release
0707010000000A000081A400000000000000000000000168975E36000018C7000000000000000000000000000000000000001900000000tfupdate-0.9.2/CLAUDE.md# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
tfupdate is a CLI tool that updates version constraints for Terraform and OpenTofu configurations. It can update core versions, providers, modules, and dependency lock files without requiring Terraform/OpenTofu CLI.
## Development Commands
### Build and Install
```bash
make build # Build binary to bin/tfupdate
make install # Install to $GOPATH/bin
go install # Alternative install method
```
### Testing
```bash
make test # Run unit tests
make testacc # Run acceptance tests (requires install first)
make testacc-all # Run all acceptance test suites
make lint # Run golangci-lint
make check # Run both lint and test
```
### Dependencies
```bash
make deps # Download Go modules
go mod download # Alternative dependency download
```
## Code Architecture
### Core Components
**Command Layer** (`/command/`)
- CLI interface using Mitchell Hashimoto's CLI library
- Commands: `terraform.go`, `opentofu.go`, `provider.go`, `module.go`, `lock.go`, `release.go`
**Business Logic** (`/tfupdate/`)
- Main updater interfaces and implementations
- `update.go` - Factory pattern for creating appropriate updaters
- `context.go`, `option.go` - Configuration management
- `file.go`, `hclwrite.go` - HCL file manipulation using `github.com/hashicorp/hcl/v2`
**Version Management** (`/release/`)
- Integrations with GitHub, GitLab, Terraform Registry, OpenTofu Registry
- `version.go` - Version parsing with `github.com/hashicorp/go-version`
**Registry Client** (`/tfregistry/`)
- HTTP clients for Terraform and OpenTofu registries
- Separate provider and module API handlers
**Lock File Handling** (`/lock/`)
- Dependency lock file updates without CLI tools
- Provider package downloading and hash calculation
- Performance-optimized with in-memory caching
### Key Patterns
- **Interface-driven design** - Heavy use of interfaces for testability (Updater, Release, etc.)
- **Factory pattern** - `tfupdate.NewUpdater()` creates appropriate updater based on type
- **Mock implementations** - Comprehensive mocking for external dependencies
- **Context-aware operations** - Proper error handling and context propagation
### File Types Handled
- **HCL files** (`.tf`) - Terraform/OpenTofu configurations
- **Lock files** (`.terraform.lock.hcl`) - Dependency constraints and hashes
- **Registry sources** - GitHub releases, GitLab releases, Terraform/OpenTofu registries
## Environment Variables
```bash
GITHUB_TOKEN=<token> # For private GitHub repositories
GITLAB_TOKEN=<token> # For GitLab repositories (with api permissions)
GITLAB_BASE_URL=<url> # For GitLab instances (default: https://gitlab.com/api/v4/)
TFREGISTRY_BASE_URL=<url> # For OpenTofu registry (set to https://registry.opentofu.org/)
```
## Test Infrastructure
- **Unit tests** - Comprehensive coverage with `*_test.go` files
- **Integration tests** - Acceptance tests in `/scripts/testacc/`
- **Test fixtures** - Sample configurations in `/test-fixtures/`
- **Mock interfaces** - Isolated testing of external dependencies
- **CI matrix testing** - Multiple Terraform (0.14.11-1.11.3) and OpenTofu (1.6.3-1.9.1) versions
## Dependencies
Key external libraries:
- `github.com/hashicorp/hcl/v2` - HCL parsing and writing
- `github.com/mitchellh/cli` - Command-line interface framework
- `github.com/hashicorp/go-version` - Semantic versioning
- `github.com/google/go-github/v28` - GitHub API client
- `github.com/xanzy/go-gitlab` - GitLab API client
- `github.com/spf13/afero` - Abstract file system interface (for testing)
## Build Requirements
- Go 1.24+
- golangci-lint (for linting)
- Make (for build automation)
## AI Agent Development Rules
When working on this codebase, AI agents must follow these strict guidelines:
### Language Requirements
- **ALL documentation, comments, commit messages, and code-related text MUST be written in English**
- This applies regardless of the language used in AI agent instructions
- Maintain consistency with the existing English-only codebase
- Exception: User-facing error messages may be localized if explicitly required
### Code Quality Standards
- Follow existing code patterns and architectural decisions
- Maintain high test coverage (aim for >85% on new code)
- Use interface-driven design for new components
- Implement proper error handling with context propagation
- Add comprehensive unit tests for all new functionality
### Documentation Standards
- Update relevant documentation when making changes
- Use clear, concise English in all comments
- Document complex algorithms and business logic
- Keep CLAUDE.md updated with architectural changes
- Follow existing comment style and patterns
- Use ASCII characters only in internal documentation (no emojis or special symbols)
- ADR (Architecture Decision Records) should be stored in `docs/adr/` with YYYYMMDD prefix
- Avoid redundant metadata in ADRs (no Date/Status/Deciders headers)
### Testing Requirements
- Write tests before implementing features (TDD approach)
- Ensure all tests pass before submitting changes
- Include both positive and negative test cases
- Mock external dependencies appropriately
- Run `make check` to verify code quality
### Project Management
- Use checkboxes in TODO lists to track completion status
- Avoid separate "Completed Items" sections (git history provides completion dates)
- Store project planning and analysis documents in appropriate directories:
- Technical decisions: `docs/adr/`
- AI-specific project tasks: `.claude/projects/{project_name}/`
- Organize multiple concurrent projects under `.claude/projects/` directory
## Active Projects
This section tracks ongoing improvement projects for the codebase.
### AI Agent Optimization
Optimizing the codebase for AI agent coding efficiency through improved test coverage, documentation, and development experience.
**Resources:**
- Task list: `.claude/projects/ai_agent_optimization/TODO.md`
- Analysis & plan: `docs/adr/20250614_ai_agent_optimization.md`
**Scope:** Test coverage improvements, architecture documentation, code readability enhancements, and development tooling standardization.0707010000000B000081A400000000000000000000000168975E3600000352000000000000000000000000000000000000001A00000000tfupdate-0.9.2/Dockerfile# tfupdate
FROM golang:1.24-alpine3.21 AS tfupdate
RUN apk --no-cache add make git
WORKDIR /work
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make build
# hub
# The linux binary for hub can not run on alpine.
# So we need to build it from source.
# https://github.com/github/hub/issues/1818
FROM golang:1.24-alpine3.21 AS hub
RUN apk add --no-cache bash git
RUN git clone https://github.com/github/hub /work
WORKDIR /work
RUN ./script/build -o bin/hub
# runtime
# Note: Required Tools for Primary Containers on CircleCI
# https://circleci.com/docs/2.0/custom-images/#required-tools-for-primary-containers
FROM alpine:3.21
RUN apk --no-cache add bash git openssh-client tar gzip ca-certificates jq openssl curl
COPY --from=tfupdate /work/bin/tfupdate /usr/local/bin/
COPY --from=hub /work/bin/hub /usr/local/bin/
ENTRYPOINT ["tfupdate"]
0707010000000C000081A400000000000000000000000168975E36000003F8000000000000000000000000000000000000001E00000000tfupdate-0.9.2/Dockerfile.devARG TERRAFORM_VERSION=latest
FROM hashicorp/terraform:$TERRAFORM_VERSION AS terraform
FROM alpine:3.21 AS opentofu
ARG OPENTOFU_VERSION=latest
ADD https://get.opentofu.org/install-opentofu.sh /install-opentofu.sh
RUN chmod +x /install-opentofu.sh
RUN apk add gpg gpg-agent
RUN ./install-opentofu.sh --install-method standalone --opentofu-version $OPENTOFU_VERSION --install-path /usr/local/bin --symlink-path -
# tfupdate
FROM golang:1.24-alpine3.21 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/
COPY --from=opentofu /usr/local/bin/tofu /usr/local/bin/
WORKDIR /work
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make install
0707010000000D000081A400000000000000000000000168975E360000043A000000000000000000000000000000000000001700000000tfupdate-0.9.2/LICENSEThe MIT License (MIT)
Copyright (c) 2019 Masayuki Morita
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
0707010000000E000081A400000000000000000000000168975E3600000251000000000000000000000000000000000000001800000000tfupdate-0.9.2/MakefileNAME := tfupdate
.DEFAULT_GOAL := build
.PHONY: deps
deps:
go mod download
.PHONY: build
build: deps
go build -o bin/$(NAME)
.PHONY: install
install: deps
go install
.PHONY: lint
lint:
golangci-lint run ./...
.PHONY: test
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
0707010000000F000081A400000000000000000000000168975E3600004351000000000000000000000000000000000000001900000000tfupdate-0.9.2/README.md# tfupdate
[](LICENSE)
[](https://github.com/minamijoyo/tfupdate/releases/latest)
[](https://godoc.org/github.com/minamijoyo/tfupdate)
## Features
- Update version constraints of Terraform core, OpenTofu core, providers, and modules
- Update dependency lock files (.terraform.lock.hcl) without Terraform / OpenTofu CLI
- Update all your Terraform / OpenTofu configurations and lock files recursively under a given directory
- Get the latest release version from the GitHub, GitLab, Terraform Registry, or OpenTofu Registry
- Terraform v0.12+ / OpenTofu v1.6+ support
If you integrate tfupdate with your favorite CI or job scheduler, you can check the latest release daily and create a Pull Request automatically.
## Why?
It is a best practice to break your Terraform configuration and state into small pieces to minimize the impact of an accident.
It is also recommended to lock versions of Terraform core, providers and modules to avoid unexpected breaking changes.
If you decided to lock version constraints, you probably want to keep them up-to-date frequently to reduce the risk of version upgrade failures.
It's easy to update a single directory, but what if they are scattered across multiple directories?
That is why I wrote a tool which parses Terraform configurations and updates all version constraints at once.
## Install
### macOS
If you are a macOS user, you can install `tfupdate` via either [Homebrew](https://brew.sh) or [MacPorts](https://www.macports.org):
#### Homebrew
```bash
$ brew install minamijoyo/tfupdate/tfupdate
```
#### MacPorts
```bash
$ sudo port install tfupdate
```
### Download
Download the latest compiled binaries and put it anywhere in your executable path.
https://github.com/minamijoyo/tfupdate/releases
### Source
If you have Go 1.24+ development environment:
```
$ go install github.com/minamijoyo/tfupdate@latest
$ tfupdate --version
```
### Docker
You can also run it with Docker:
```
$ docker run -it --rm minamijoyo/tfupdate --version
```
## Usage
```
$ tfupdate --help
Usage: tfupdate [--version] [--help] <command> [<args>]
Available commands are:
lock Update dependency lock files
module Update version constraints for module
opentofu Update version constraints for opentofu
provider Update version constraints for provider
release Get release version information
terraform Update version constraints for terraform
```
### terraform
```
$ tfupdate terraform --help
Usage: tfupdate terraform [options] <PATH>
Arguments
PATH A path of file or directory to update
Options:
-v --version A new version constraint (default: latest)
If the version is omitted, the latest version is automatically checked and set.
-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.
```
If you have `main.tf` like the following:
```
$ cat main.tf
terraform {
required_version = "0.12.15"
}
```
Execute the following command:
```
$ tfupdate terraform -v 0.12.16 main.tf
```
```
$ cat main.tf
terraform {
required_version = "0.12.16"
}
```
A value of version flag accepts any string literal. You can also pass a [version constraint](https://www.terraform.io/language/expressions/version-constraints):
```
$ tfupdate terraform -v "~> 1.0" main.tf
$ cat main.tf
terraform {
required_version = "~> 1.0"
}
```
If you want to update all your Terraform configurations under the current directory recursively,
use `-r (--recursive)` option:
```
$ tfupdate terraform -v 0.12.16 -r ./
```
You can also ignore some path patterns with `-i (--ignore-path)` option:
```
$ tfupdate terraform -v 0.12.16 -i modules/ -r ./
```
If the version is omitted, the latest version is automatically checked and set.
```
$ tfupdate terraform -r ./
```
### opentofu
```
$ tfupdate opentofu --help
Usage: tfupdate opentofu [options] <PATH>
Arguments
PATH A path of file or directory to update
Options:
-v --version A new version constraint (default: latest)
If the version is omitted, the latest version is automatically checked and set.
-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.
```
If you have `main.tf` like the following:
```
$ cat main.tf
terraform {
required_version = "1.8.0"
}
```
Execute the following command:
```
$ tfupdate opentofu -v 1.9.0 main.tf
```
```
$ cat main.tf
terraform {
required_version = "1.9.0"
}
```
### provider
```
$ tfupdate provider --help
Usage: tfupdate provider [options] <PROVIDER_NAME> <PATH>
Arguments
PROVIDER_NAME A name of provider (e.g. aws or integrations/github)
PATH A path of file or directory to update
Options:
-v --version A new version constraint (default: latest)
If the version is omitted, the latest version is automatically checked and set.
-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.
```
```
$ cat main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.70.0"
}
}
}
$ tfupdate provider aws -v 3.74.0 main.tf
$ cat main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.74.0"
}
}
}
```
A value of version flag accepts any string literal. You can also pass a [version constraint](https://www.terraform.io/language/expressions/version-constraints):
```
$ tfupdate provider aws -v "~> 3.0" main.tf
$ cat main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
```
For updating the dependency lock file (.terraform.lock.hcl), use the `tfupdate lock` command.
### module
```
$ tfupdate module --help
Usage: tfupdate module [options] <MODULE_NAME> <PATH>
Arguments
MODULE_NAME A name of module or a regular expression in RE2 syntax
e.g.
terraform-aws-modules/vpc/aws
git::https://example.com/vpc.git
git::https://example\.com/.+
PATH A path of file or directory to update
Options:
-v --version A new version constraint (required)
Automatic latest version resolution is not currently supported for modules.
-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.
--source-match-type Define how to match MODULE_NAME to the module source URLs. Valid values are "full" or "regex". (default: full)
```
```
$ cat main.tf
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "2.14.0"
bucket = "my-s3-bucket"
acl = "private"
versioning = {
enabled = true
}
}
$ tfupdate module -v 2.14.1 terraform-aws-modules/s3-bucket/aws main.tf
$ cat main.tf
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "2.14.1"
bucket = "my-s3-bucket"
acl = "private"
versioning = {
enabled = true
}
}
```
A value of version flag accepts any string literal. You can also pass a [version constraint](https://www.terraform.io/language/expressions/version-constraints):
```
$ tfupdate module -v "~> 2.14.1" terraform-aws-modules/s3-bucket/aws main.tf
$ cat main.tf
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "~> 2.14.1"
bucket = "my-s3-bucket"
acl = "private"
versioning = {
enabled = true
}
}
```
### release
```
$ tfupdate release --help
Usage: tfupdate release <subcommand> [options] [args]
This command has subcommands for release version information.
Subcommands:
latest Get the latest release version
list Get a list of release versions
```
```
$ tfupdate release latest --help
Usage: tfupdate release latest [options] <SOURCE>
Arguments
SOURCE A path of release data source.
Valid format depends on --source-type option.
- github or gitlab:
owner/repo
e.g. terraform-providers/terraform-provider-aws
- tfregistryModule:
namespace/name/provider
e.g. terraform-aws-modules/vpc/aws
- tfregistryProvider:
namespace/type
e.g. hashicorp/aws
Options:
-s --source-type A type of release data source.
Valid values are
- github (default)
- gitlab
- tfregistryModule
- tfregistryProvider
```
```
$ tfupdate release latest terraform-providers/terraform-provider-aws
2.40.0
```
If you want to access private repositories on GitHub, export your access token to the `GITHUB_TOKEN` environment variable.
If you want to access public or private repositories on GitLab, export your access token with api permissions to the `GITLAB_TOKEN` environment variable. If you are using an instance that is not `https://gitlab.com`, set the correct base URL to the `GITLAB_BASE_URL` environment variable (defaults to `https://gitlab.com/api/v4/`).
If you want to use the public OpenTofu registry, set the `TFREGISTRY_BASE_URL` environment variable to `https://registry.opentofu.org/`.
```
$ tfupdate release list --help
Usage: tfupdate release list [options] <SOURCE>
Arguments
SOURCE A path of release data source.
Valid format depends on --source-type option.
- github or gitlab:
owner/repo
e.g. terraform-providers/terraform-provider-aws
- tfregistryModule:
namespace/name/provider
e.g. terraform-aws-modules/vpc/aws
- tfregistryProvider:
namespace/type
e.g. hashicorp/aws
Options:
-s --source-type A type of release data source.
Valid values are
- github (default)
- gitlab
- tfregistryModule
- tfregistryProvider
-n --max-length The maximum length of list.
```
```
$ tfupdate release list -n 5 hashicorp/terraform
0.12.17
0.12.18
0.12.19
0.12.20
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] <PATH>
Arguments
PATH A relative 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.
```
If you want to use the public OpenTofu registry, set the `TFREGISTRY_BASE_URL` environment variable to `https://registry.opentofu.org/`.
```
$ export TFREGISTRY_BASE_URL=https://registry.opentofu.org/
```
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.
An example for tfupdate with CircleCI is available:
https://github.com/minamijoyo/tfupdate-circleci-example
You can also use a CircleCI orb:
https://github.com/masutaka/circleci-tfupdate-orb
## License
MIT
07070100000010000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001700000000tfupdate-0.9.2/command07070100000011000081A400000000000000000000000168975E360000043F000000000000000000000000000000000000001E00000000tfupdate-0.9.2/command/env.gopackage command
// Env is a set of configurations read from environment variables.
type Env struct {
// GitHubBaseURL is a base URL for GtiHub API requests.
// Defaults to the public GitHub API.
GitHubBaseURL string `envconfig:"GITHUB_BASE_URL" default:"https://api.github.com/"`
// GitHubToken is a personal access token for GitHub.
// This allows access to a private repository.
GitHubToken string `envconfig:"GITHUB_TOKEN"`
// GitLabBaseURL is a base URL for GitLab API requests.
// Defaults to the public GitLab API.
GitLabBaseURL string `envconfig:"GITLAB_BASE_URL" default:"https://gitlab.com/api/v4/"`
// GitLabToken is a personal access token for GitLab.
// This is needed for public and private projects on all instances.
GitLabToken string `envconfig:"GITLAB_TOKEN"`
// TFRegistryBaseURL is a base URL for Terraform registry.
// Defaults to the public Terraform registry.
// To use the public OpenTofu registry, set this to `https://registry.opentofu.org/`.
TFRegistryBaseURL string `envconfig:"TFREGISTRY_BASE_URL" default:"https://registry.terraform.io/"`
}
07070100000012000081A400000000000000000000000168975E3600000CB4000000000000000000000000000000000000001F00000000tfupdate-0.9.2/command/lock.gopackage command
import (
"context"
"fmt"
"log"
"path/filepath"
"strings"
"github.com/kelseyhightower/envconfig"
"github.com/minamijoyo/tfupdate/tfregistry"
"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 filepath.IsAbs(c.path) {
c.UI.Error("The PATH argument should be a relative path, not an absolute path")
c.UI.Error(c.Help())
return 1
}
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")
// Fetch environment variables
var env Env
err := envconfig.Process("", &env)
if err != nil {
c.UI.Error(fmt.Sprintf("failed to fetch environment variables: %s", err))
return 1
}
// Create tfregistry.Config
tfregistryConfig := tfregistry.Config{
BaseURL: env.TFRegistryBaseURL,
}
option, err := tfupdate.NewOption("lock", "", "", c.platforms, c.recursive, c.ignorePaths, "", tfregistryConfig)
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] <PATH>
Arguments
PATH A relative 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"
}
07070100000013000081A400000000000000000000000168975E36000005D8000000000000000000000000000000000000001F00000000tfupdate-0.9.2/command/meta.gopackage command
import (
"fmt"
"github.com/kelseyhightower/envconfig"
"github.com/minamijoyo/tfupdate/release"
"github.com/minamijoyo/tfupdate/tfregistry"
"github.com/mitchellh/cli"
"github.com/spf13/afero"
)
// Meta are the meta-options that are available on all or most commands.
type Meta struct {
// UI is a user interface representing input and output.
UI cli.Ui
// Fs is an afero filesystem.
Fs afero.Fs
}
// newRelease is a factory method which returns an Release implementation.
func newRelease(sourceType string, source string) (release.Release, error) {
var env Env
err := envconfig.Process("", &env)
if err != nil {
return nil, fmt.Errorf("failed to fetch environment variables: %s", err)
}
switch sourceType {
case "github":
config := release.GitHubConfig{
BaseURL: env.GitHubBaseURL,
Token: env.GitHubToken,
}
return release.NewGitHubRelease(source, config)
case "gitlab":
config := release.GitLabConfig{
BaseURL: env.GitLabBaseURL,
Token: env.GitLabToken,
}
return release.NewGitLabRelease(source, config)
case "tfregistryModule":
config := tfregistry.Config{
BaseURL: env.TFRegistryBaseURL,
}
return release.NewTFRegistryModuleRelease(source, config)
case "tfregistryProvider":
config := tfregistry.Config{
BaseURL: env.TFRegistryBaseURL,
}
return release.NewTFRegistryProviderRelease(source, config)
default:
return nil, fmt.Errorf("failed to new release data source. unknown type: %s", sourceType)
}
}
07070100000014000081A400000000000000000000000168975E3600000D40000000000000000000000000000000000000002100000000tfupdate-0.9.2/command/module.gopackage command
import (
"context"
"fmt"
"log"
"strings"
"github.com/minamijoyo/tfupdate/tfregistry"
"github.com/minamijoyo/tfupdate/tfupdate"
flag "github.com/spf13/pflag"
)
// ModuleCommand is a command which update version constraints for module.
type ModuleCommand struct {
Meta
name string
version string
path string
recursive bool
ignorePaths []string
sourceMatchType string
}
// Run runs the procedure of this command.
func (c *ModuleCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("module", flag.ContinueOnError)
cmdFlags.StringVarP(&c.version, "version", "v", "", "A new version constraint")
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")
cmdFlags.StringVar(&c.sourceMatchType, "source-match-type", "full", "Define how to match module source URLs. Valid values are \"full\" or \"regex\".")
if err := cmdFlags.Parse(args); err != nil {
c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err))
return 1
}
if len(cmdFlags.Args()) != 2 {
c.UI.Error(fmt.Sprintf("The command expects 2 arguments, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}
c.name = cmdFlags.Arg(0)
c.path = cmdFlags.Arg(1)
v := c.version
if len(v) == 0 {
// For modules, automatic latest version resolution is not simple.
// To implement, we will probably need to get information from the Terraform Registry.
c.UI.Error("A new version constraint is required. Automatic latest version resolution is not currently supported for modules.")
return 1
}
log.Printf("[INFO] Update module %s to %s", c.name, v)
option, err := tfupdate.NewOption("module", c.name, v, []string{}, c.recursive, c.ignorePaths, c.sourceMatchType, tfregistry.Config{})
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 *ModuleCommand) Help() string {
helpText := `
Usage: tfupdate module [options] <MODULE_NAME> <PATH>
Arguments
MODULE_NAME A name of module or a regular expression in RE2 syntax
e.g.
terraform-aws-modules/vpc/aws
git::https://example.com/vpc.git
git::https://example\.com/.+
PATH A path of file or directory to update
Options:
-v --version A new version constraint (required)
Automatic latest version resolution is not currently supported for modules.
-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.
--source-match-type Define how to match MODULE_NAME to the module source URLs. Valid values are "full" or "regex". (default: full)
`
return strings.TrimSpace(helpText)
}
// Synopsis returns one-line help text.
func (c *ModuleCommand) Synopsis() string {
return "Update version constraints for module"
}
07070100000015000081A400000000000000000000000168975E3600000A9C000000000000000000000000000000000000002300000000tfupdate-0.9.2/command/opentofu.gopackage command
import (
"context"
"fmt"
"log"
"strings"
"github.com/minamijoyo/tfupdate/release"
"github.com/minamijoyo/tfupdate/tfregistry"
"github.com/minamijoyo/tfupdate/tfupdate"
flag "github.com/spf13/pflag"
)
// OpenTofuCommand is a command which update version constraints for OpenTofu.
type OpenTofuCommand struct {
Meta
version string
path string
recursive bool
ignorePaths []string
}
// Run runs the procedure of this command.
func (c *OpenTofuCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("opentofu", flag.ContinueOnError)
cmdFlags.StringVarP(&c.version, "version", "v", "latest", "A new version constraint")
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 argument, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}
c.path = cmdFlags.Arg(0)
v := c.version
if v == "latest" {
r, err := newRelease("github", "opentofu/opentofu")
if err != nil {
c.UI.Error(err.Error())
return 1
}
v, err = release.Latest(context.Background(), r)
if err != nil {
c.UI.Error(err.Error())
return 1
}
}
log.Printf("[INFO] Update opentofu to %s", v)
option, err := tfupdate.NewOption("opentofu", "", v, []string{}, c.recursive, c.ignorePaths, "", tfregistry.Config{})
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 *OpenTofuCommand) Help() string {
helpText := `
Usage: tfupdate opentofu [options] <PATH>
Arguments
PATH A path of file or directory to update
Options:
-v --version A new version constraint (default: latest)
If the version is omitted, the latest version is automatically checked and set.
-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 *OpenTofuCommand) Synopsis() string {
return "Update version constraints for opentofu"
}
07070100000016000081A400000000000000000000000168975E3600000C24000000000000000000000000000000000000002300000000tfupdate-0.9.2/command/provider.gopackage command
import (
"context"
"fmt"
"log"
"strings"
"github.com/minamijoyo/tfupdate/release"
"github.com/minamijoyo/tfupdate/tfregistry"
"github.com/minamijoyo/tfupdate/tfupdate"
flag "github.com/spf13/pflag"
)
// ProviderCommand is a command which update version constraints for provider.
type ProviderCommand struct {
Meta
name string
version string
path string
recursive bool
ignorePaths []string
}
// Run runs the procedure of this command.
func (c *ProviderCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("provider", flag.ContinueOnError)
cmdFlags.StringVarP(&c.version, "version", "v", "latest", "A new version constraint")
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()) != 2 {
c.UI.Error(fmt.Sprintf("The command expects 2 arguments, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}
c.name = cmdFlags.Arg(0)
c.path = cmdFlags.Arg(1)
v := c.version
if v == "latest" {
source := ""
if strings.Contains(c.name, "/") {
namespace, name, _ := strings.Cut(c.name, "/")
source = fmt.Sprintf("%s/terraform-provider-%s", namespace, name)
} else {
source = fmt.Sprintf("hashicorp/terraform-provider-%s", c.name)
}
r, err := newRelease("github", source)
if err != nil {
c.UI.Error(err.Error())
return 1
}
v, err = release.Latest(context.Background(), r)
if err != nil {
c.UI.Error(err.Error())
return 1
}
}
log.Printf("[INFO] Update provider %s to %s", c.name, v)
option, err := tfupdate.NewOption("provider", c.name, v, []string{}, c.recursive, c.ignorePaths, "", tfregistry.Config{})
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 *ProviderCommand) Help() string {
helpText := `
Usage: tfupdate provider [options] <PROVIDER_NAME> <PATH>
Arguments
PROVIDER_NAME A name of provider (e.g. aws or integrations/github)
PATH A path of file or directory to update
Options:
-v --version A new version constraint (default: latest)
If the version is omitted, the latest version is automatically checked and set.
-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 *ProviderCommand) Synopsis() string {
return "Update version constraints for provider"
}
07070100000017000081A400000000000000000000000168975E36000002CB000000000000000000000000000000000000002200000000tfupdate-0.9.2/command/release.gopackage command
import (
"strings"
"github.com/mitchellh/cli"
)
// ReleaseCommand is a command which just shows help for subcommands.
type ReleaseCommand struct {
Meta
}
// Run runs the procedure of this command.
func (c *ReleaseCommand) Run(args []string) int { // nolint revive unused-parameter
return cli.RunResultHelp
}
// Help returns long-form help text.
func (c *ReleaseCommand) Help() string {
helpText := `
Usage: tfupdate release <subcommand> [options] [args]
This command has subcommands for release version information.
`
return strings.TrimSpace(helpText)
}
// Synopsis returns one-line help text.
func (c *ReleaseCommand) Synopsis() string {
return "Get release version information"
}
07070100000018000081A400000000000000000000000168975E36000008D9000000000000000000000000000000000000002900000000tfupdate-0.9.2/command/release_latest.gopackage command
import (
"context"
"fmt"
"strings"
"github.com/minamijoyo/tfupdate/release"
flag "github.com/spf13/pflag"
)
// ReleaseLatestCommand is a command which gets the latest release version.
type ReleaseLatestCommand struct {
Meta
sourceType string
source string
}
// Run runs the procedure of this command.
func (c *ReleaseLatestCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("release latest", flag.ContinueOnError)
cmdFlags.StringVarP(&c.sourceType, "source-type", "s", "github", "A type of release data source")
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 argument, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}
c.source = cmdFlags.Arg(0)
r, err := newRelease(c.sourceType, c.source)
if err != nil {
c.UI.Error(err.Error())
return 1
}
v, err := release.Latest(context.Background(), r)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.UI.Output(v)
return 0
}
// Help returns long-form help text.
func (c *ReleaseLatestCommand) Help() string {
helpText := `
Usage: tfupdate release latest [options] <SOURCE>
Arguments
SOURCE A path of release data source.
Valid format depends on --source-type option.
- github or gitlab:
owner/repo
e.g. terraform-providers/terraform-provider-aws
- tfregistryModule:
namespace/name/provider
e.g. terraform-aws-modules/vpc/aws
- tfregistryProvider:
namespace/type
e.g. hashicorp/aws
Options:
-s --source-type A type of release data source.
Valid values are
- github (default)
- gitlab
- tfregistryModule
- tfregistryProvider
`
return strings.TrimSpace(helpText)
}
// Synopsis returns one-line help text.
func (c *ReleaseLatestCommand) Synopsis() string {
return "Get the latest release version"
}
07070100000019000081A400000000000000000000000168975E3600000A32000000000000000000000000000000000000002700000000tfupdate-0.9.2/command/release_list.gopackage command
import (
"context"
"fmt"
"strings"
"github.com/minamijoyo/tfupdate/release"
flag "github.com/spf13/pflag"
)
// ReleaseListCommand is a command which gets a list of release versions.
type ReleaseListCommand struct {
Meta
maxLength int
preRelease bool
sourceType string
source string
}
// Run runs the procedure of this command.
func (c *ReleaseListCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("release list", flag.ContinueOnError)
cmdFlags.IntVarP(&c.maxLength, "max-length", "n", 10, "the maximum length of list")
cmdFlags.BoolVar(&c.preRelease, "pre-release", false, "show pre-releases")
cmdFlags.StringVarP(&c.sourceType, "source-type", "s", "github", "A type of release data source")
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 argument, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}
c.source = cmdFlags.Arg(0)
r, err := newRelease(c.sourceType, c.source)
if err != nil {
c.UI.Error(err.Error())
return 1
}
versions, err := release.List(context.Background(), r, c.maxLength, c.preRelease)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.UI.Output(strings.Join(versions, "\n"))
return 0
}
// Help returns long-form help text.
func (c *ReleaseListCommand) Help() string {
helpText := `
Usage: tfupdate release list [options] <SOURCE>
Arguments
SOURCE A path of release data source.
Valid format depends on --source-type option.
- github or gitlab:
owner/repo
e.g. terraform-providers/terraform-provider-aws
- tfregistryModule:
namespace/name/provider
e.g. terraform-aws-modules/vpc/aws
- tfregistryProvider:
namespace/type
e.g. hashicorp/aws
Options:
-s --source-type A type of release data source.
Valid values are
- github (default)
- gitlab
- tfregistryModule
- tfregistryProvider
-n --max-length The maximum length of list.
--pre-release Show pre-releases. (default: false)
`
return strings.TrimSpace(helpText)
}
// Synopsis returns one-line help text.
func (c *ReleaseListCommand) Synopsis() string {
return "Get a list of release versions"
}
0707010000001A000081A400000000000000000000000168975E3600000AA9000000000000000000000000000000000000002400000000tfupdate-0.9.2/command/terraform.gopackage command
import (
"context"
"fmt"
"log"
"strings"
"github.com/minamijoyo/tfupdate/release"
"github.com/minamijoyo/tfupdate/tfregistry"
"github.com/minamijoyo/tfupdate/tfupdate"
flag "github.com/spf13/pflag"
)
// TerraformCommand is a command which update version constraints for terraform.
type TerraformCommand struct {
Meta
version string
path string
recursive bool
ignorePaths []string
}
// Run runs the procedure of this command.
func (c *TerraformCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("terraform", flag.ContinueOnError)
cmdFlags.StringVarP(&c.version, "version", "v", "latest", "A new version constraint")
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 argument, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}
c.path = cmdFlags.Arg(0)
v := c.version
if v == "latest" {
r, err := newRelease("github", "hashicorp/terraform")
if err != nil {
c.UI.Error(err.Error())
return 1
}
v, err = release.Latest(context.Background(), r)
if err != nil {
c.UI.Error(err.Error())
return 1
}
}
log.Printf("[INFO] Update terraform to %s", v)
option, err := tfupdate.NewOption("terraform", "", v, []string{}, c.recursive, c.ignorePaths, "", tfregistry.Config{})
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 *TerraformCommand) Help() string {
helpText := `
Usage: tfupdate terraform [options] <PATH>
Arguments
PATH A path of file or directory to update
Options:
-v --version A new version constraint (default: latest)
If the version is omitted, the latest version is automatically checked and set.
-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 *TerraformCommand) Synopsis() string {
return "Update version constraints for terraform"
}
0707010000001B000081A400000000000000000000000168975E3600000163000000000000000000000000000000000000001C00000000tfupdate-0.9.2/compose.yamlservices:
tfupdate:
build:
context: .
dockerfile: ./Dockerfile.dev
args:
TERRAFORM_VERSION: ${TERRAFORM_VERSION:-latest}
OPENTOFU_VERSION: ${OPENTOFU_VERSION:-latest}
volumes:
- ".:/work"
environment:
CGO_ENABLED: 0 # disable cgo for go test
TFUPDATE_EXEC_PATH:
TFREGISTRY_BASE_URL:
0707010000001C000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001400000000tfupdate-0.9.2/docs0707010000001D000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001800000000tfupdate-0.9.2/docs/adr0707010000001E000081A400000000000000000000000168975E36000015D7000000000000000000000000000000000000003A00000000tfupdate-0.9.2/docs/adr/20250614_ai_agent_optimization.md# ADR: AI Agent Optimization for tfupdate
## Context
The tfupdate project needs optimization to maximize AI agent coding efficiency. AI agents like Claude Code are increasingly used for software development, and codebases should be structured to enable these agents to work effectively.
## Problem Statement
While tfupdate has a solid foundation with interface-driven design and good test coverage in core packages, several areas need improvement for AI agent optimization:
1. **Test Coverage Gaps**: main.go (0%) and command/ package (0%) lack unit tests
2. **Documentation Deficits**: Missing architecture diagrams, API specifications, and design decision records
3. **Code Readability**: Some functions are lengthy, naming could be more expressive
4. **Development Experience**: Limited linting rules, basic development environment setup
## Ideal State Analysis
An AI-agent-optimized codebase should have:
### Architecture & Design
- Clear separation of responsibilities
- Interface-driven design (already implemented)
- Consistent design patterns (factory pattern in use)
- Clear dependency relationships without circular dependencies
### Documentation & Readability
- Comprehensive documentation (README, architecture diagrams, API specs)
- Self-explanatory code with intention-revealing names
- Appropriate comments for complex logic
- Clear API specifications for external interfaces
### Testing & Quality Assurance
- High test coverage (>85% target)
- Mocked external dependencies (partially implemented)
- Automated CI/CD with lint, test, build
- Consistent code style enforcement
### Developer Experience
- Clear development procedures and tooling
- Consistent error handling patterns
- Comprehensive logging for debugging
- Simplified development environment setup
## Current State Assessment
### Strengths
- **Excellent foundational architecture**
- Interface-driven design (Updater, Release, etc.)
- Factory pattern implementation
- Clear responsibility separation (command/tfupdate/release/lock)
- Good test coverage in core packages (82-89%)
- **Development & CI environment**
- Comprehensive Makefile
- golangci-lint configuration
- Acceptance test suite
- Clear guidance via CLAUDE.md
- **Code quality**
- Proper error handling
- Rich mock implementations
- Consistent package structure
### Gaps Identified
#### High Priority
1. **Uneven test coverage**
- main package: 0.0%
- command package: 0.0%
- Insufficient testing outside core logic
2. **Documentation gaps**
- No architecture diagrams
- Missing API/interface specifications
- Lack of design decision records
3. **Code readability**
- Some functions could be more expressive
- Complex logic lacks explanatory comments
#### Medium Priority
4. **Development experience**
- Basic golangci-lint rules only
- Room for debugging information improvement
- Development environment setup could be simplified
5. **Automation & CI enhancement**
- Security scanning
- Dependency vulnerability checks
- Performance testing
## Decision
Implement a phased approach to AI agent optimization:
### Phase 1: Foundation Strengthening (Highest Priority)
1. **Test Coverage Improvement**
- Refactor main.go for testability
- Add comprehensive unit tests for command/ package
- Standardize test helper functions
2. **Architecture Documentation Creation**
- Create system design diagrams
- Document interface specifications
- Record design decisions (ADRs)
3. **Code Readability Enhancement**
- Split lengthy functions (>100 lines)
- Improve function/variable naming
- Add comments to complex logic
### Phase 2: Developer Experience Enhancement (High Priority)
4. **Strengthen Linting & Static Analysis**
5. **Standardize Development Environment**
6. **Improve Error Handling & Logging**
### Phase 3: Automation & Monitoring (Medium Priority)
7. **Strengthen CI/CD Pipeline**
8. **Metrics & Monitoring**
### Phase 4: AI Optimization (Future Investment)
9. **Code Generation Support**
10. **Documentation Automation**
## Implementation Guidelines
- **Language Requirement**: ALL documentation, comments, and code-related text MUST be written in English
- **Testing**: Follow TDD approach - write tests before implementation
- **Quality**: Maintain >85% test coverage on new code
- **Compatibility**: Minimize impact on existing functionality
- **Documentation**: Update documentation simultaneously with implementation
## Consequences
### Positive
- Improved AI agent coding efficiency
- Better code maintainability for human developers
- Higher code quality and test coverage
- Comprehensive documentation for onboarding
- Standardized development practices
### Negative
- Initial time investment required for implementation
- Temporary disruption during refactoring
- Need to maintain additional documentation
### Risks & Mitigation
- **Risk**: Breaking existing functionality during refactoring
- **Mitigation**: Comprehensive test suite and incremental changes
- **Risk**: Documentation becoming outdated
- **Mitigation**: Automated documentation generation where possible
## Monitoring & Success Criteria
- Test coverage metrics (target: >85% overall)
- Code quality metrics (complexity, duplication)
- Development velocity improvements
- Reduced time for AI agents to complete tasks
- Improved code review efficiency
## References
- Project TODO list: `.claude/projects/ai_agent_optimization/TODO.md`
- Development guidelines: `CLAUDE.md`
- Current test coverage: Core packages 82-89%, main/command 0%0707010000001F000081A400000000000000000000000168975E360000077A000000000000000000000000000000000000001600000000tfupdate-0.9.2/go.modmodule github.com/minamijoyo/tfupdate
go 1.24
require (
github.com/davecgh/go-spew v1.1.1
github.com/google/go-cmp v0.6.0
github.com/google/go-github/v28 v28.1.1
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/hashicorp/logutils v1.0.0
github.com/hashicorp/terraform-registry-address v0.2.0
github.com/hashicorp/terraform-svchost v0.0.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/minamijoyo/terraform-config-inspect v0.0.0-20250505010908-6ad6eb27d3c9
github.com/mitchellh/cli v1.0.0
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.16.3
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/mod v0.26.0
golang.org/x/oauth2 v0.4.0
)
require (
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.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.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/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/posener/complete v1.1.1 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)
07070100000020000081A400000000000000000000000168975E360000CD68000000000000000000000000000000000000001600000000tfupdate-0.9.2/go.sumcloud.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=
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/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
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/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
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.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/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/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.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.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
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-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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/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/minamijoyo/terraform-config-inspect v0.0.0-20250505010908-6ad6eb27d3c9 h1:6trjsdYNQsBXv9BShavPR047w/ufWQ6CnzlGO/nd2vo=
github.com/minamijoyo/terraform-config-inspect v0.0.0-20250505010908-6ad6eb27d3c9/go.mod h1:RhDzenR6+Mj5hmOQpw0nOiMTPDTrjLfuYAtxvcq2IVY=
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 v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
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/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/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.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
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-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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
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-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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
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-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/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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-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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
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.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=
07070100000021000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001400000000tfupdate-0.9.2/lock07070100000022000081A400000000000000000000000168975E36000009A9000000000000000000000000000000000000001C00000000tfupdate-0.9.2/lock/hash.gopackage 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
}
07070100000023000081A400000000000000000000000168975E3600001367000000000000000000000000000000000000002100000000tfupdate-0.9.2/lock/hash_test.gopackage 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)
}
})
}
}
07070100000024000081A400000000000000000000000168975E3600001A3A000000000000000000000000000000000000001D00000000tfupdate-0.9.2/lock/index.gopackage lock
import (
"context"
"fmt"
"log"
"strings"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/minamijoyo/tfupdate/tfregistry"
)
// 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
}
// NewIndexFromConfig returns a new instance of Index with the given registry config.
func NewIndexFromConfig(config tfregistry.Config) (Index, error) {
client, err := NewProviderDownloaderClient(config)
if err != nil {
return nil, err
}
index := NewIndex(client)
return index, nil
}
// NewIndex returns a new instance of Index with the given provider downloader API.
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
}
07070100000025000081A400000000000000000000000168975E3600002757000000000000000000000000000000000000002200000000tfupdate-0.9.2/lock/index_test.gopackage 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)
}
})
}
}
07070100000026000081A400000000000000000000000168975E3600001768000000000000000000000000000000000000001C00000000tfupdate-0.9.2/lock/mock.gopackage lock
import (
"archive/zip"
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/minamijoyo/tfupdate/tfregistry"
"golang.org/x/exp/slices"
)
// mockTFRegistryClient is a mock implementation of tfregistry.API
type mockTFRegistryClient struct {
metadataRes *tfregistry.ProviderPackageMetadataResponse
err error
}
var _ tfregistry.API = (*mockTFRegistryClient)(nil)
func (c *mockTFRegistryClient) ProviderPackageMetadata(_ context.Context, _ *tfregistry.ProviderPackageMetadataRequest) (*tfregistry.ProviderPackageMetadataResponse, error) {
return c.metadataRes, c.err
}
func (c *mockTFRegistryClient) ListModuleVersions(_ context.Context, _ *tfregistry.ListModuleVersionsRequest) (*tfregistry.ListModuleVersionsResponse, error) {
return nil, nil // dummy implementation as it's not used in tests
}
func (c *mockTFRegistryClient) ListProviderVersions(_ context.Context, _ *tfregistry.ListProviderVersionsRequest) (*tfregistry.ListProviderVersionsResponse, error) {
return nil, nil // dummy implementation as it's not used in tests
}
// 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 tfregistry.Config) *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,
}
}
07070100000027000081A400000000000000000000000168975E36000017FC000000000000000000000000000000000000002B00000000tfupdate-0.9.2/lock/provider_downloader.gopackage 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 tfregistry.API interface.
// It can be replaced for testing.
api tfregistry.API
// httpClient is a http client which communicates with the ProviderDownloaderAPI.
httpClient *http.Client
}
// NewProviderDownloaderClient is a factory method which returns a ProviderDownloaderClient instance.
func NewProviderDownloaderClient(config tfregistry.Config) (*ProviderDownloaderClient, error) {
api, err := tfregistry.NewClient(config)
if err != nil {
return nil, err
}
httpClient := config.HTTPClient
if httpClient == nil {
httpClient = &http.Client{}
}
return &ProviderDownloaderClient{
api: api,
httpClient: httpClient,
}, 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)
}
07070100000028000081A400000000000000000000000168975E3600001F6C000000000000000000000000000000000000003000000000tfupdate-0.9.2/lock/provider_downloader_test.gopackage 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, _ *http.Request) {
w.WriteHeader(200)
_, _ = w.Write(zipData)
})
mux.HandleFunc(shaSumsPath, func(w http.ResponseWriter, _ *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 := tfregistry.Config{}
client := newTestClient(mockServerURL, config)
client.api = tc.client
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 := tfregistry.Config{}
client := newTestClient(mockServerURL, config)
mux.HandleFunc(tc.subPath, func(w http.ResponseWriter, _ *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")
}
})
}
}
07070100000029000081A400000000000000000000000168975E3600000AE2000000000000000000000000000000000000002800000000tfupdate-0.9.2/lock/provider_version.gopackage 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
}
0707010000002A000081A400000000000000000000000168975E3600002A4D000000000000000000000000000000000000002D00000000tfupdate-0.9.2/lock/provider_version_test.gopackage 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)
}
})
}
}
0707010000002B000081A400000000000000000000000168975E360000099D000000000000000000000000000000000000001700000000tfupdate-0.9.2/main.gopackage main
import (
"fmt"
"io"
"log"
"os"
"strings"
"github.com/hashicorp/logutils"
"github.com/minamijoyo/tfupdate/command"
"github.com/mitchellh/cli"
"github.com/spf13/afero"
)
// Version is a version number.
var version = "0.9.2"
// UI is a user interface which is a global variable for mocking.
var UI cli.Ui
func init() {
UI = &cli.BasicUi{
Writer: os.Stdout,
}
}
func main() {
log.SetOutput(logOutput())
log.Printf("[INFO] CLI args: %#v", os.Args)
commands := initCommands()
args := os.Args[1:]
c := &cli.CLI{
Name: "tfupdate",
Version: version,
Args: args,
Commands: commands,
HelpWriter: os.Stdout,
Autocomplete: true,
AutocompleteInstall: "install-autocomplete",
AutocompleteUninstall: "uninstall-autocomplete",
}
exitStatus, err := c.Run()
if err != nil {
UI.Error(fmt.Sprintf("Failed to execute CLI: %s", err))
}
os.Exit(exitStatus)
}
func logOutput() io.Writer {
levels := []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"}
minLevel := os.Getenv("TFUPDATE_LOG")
// default log writer is null device.
writer := io.Discard
if minLevel != "" {
writer = os.Stderr
}
filter := &logutils.LevelFilter{
Levels: levels,
MinLevel: logutils.LogLevel(strings.ToUpper(minLevel)),
Writer: writer,
}
return filter
}
func initCommands() map[string]cli.CommandFactory {
meta := command.Meta{
UI: UI,
Fs: afero.NewOsFs(),
}
commands := map[string]cli.CommandFactory{
"terraform": func() (cli.Command, error) {
return &command.TerraformCommand{
Meta: meta,
}, nil
},
"opentofu": func() (cli.Command, error) {
return &command.OpenTofuCommand{
Meta: meta,
}, nil
},
"provider": func() (cli.Command, error) {
return &command.ProviderCommand{
Meta: meta,
}, nil
},
"module": func() (cli.Command, error) {
return &command.ModuleCommand{
Meta: meta,
}, nil
},
"lock": func() (cli.Command, error) {
return &command.LockCommand{
Meta: meta,
}, nil
},
"release": func() (cli.Command, error) {
return &command.ReleaseCommand{
Meta: meta,
}, nil
},
"release latest": func() (cli.Command, error) {
return &command.ReleaseLatestCommand{
Meta: meta,
}, nil
},
"release list": func() (cli.Command, error) {
return &command.ReleaseListCommand{
Meta: meta,
}, nil
},
}
return commands
}
0707010000002C000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001700000000tfupdate-0.9.2/release0707010000002D000081A400000000000000000000000168975E3600000FF2000000000000000000000000000000000000002100000000tfupdate-0.9.2/release/github.gopackage release
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/google/go-github/v28/github"
"golang.org/x/oauth2"
)
// GitHubAPI is an interface which calls GitHub API.
// This abstraction layer is needed for testing with mock.
type GitHubAPI interface {
// RepositoriesListReleases lists the releases for a repository.
// GitHub API docs: https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository
RepositoriesListReleases(ctx context.Context, owner, repo string, opt *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error)
}
// GitHubConfig is a set of configurations for GitHubRelease.
type GitHubConfig struct {
// api is an instance of GitHubAPI interface.
// It can be replaced for testing.
api GitHubAPI
// BaseURL is a URL for GitHub API requests.
// Defaults to the public GitHub API.
// This looks like the GitHub Enterprise support, but currently for testing purposes only.
// The GitHub Enterprise is not supported yet.
// BaseURL should always be specified with a trailing slash.
BaseURL string
// Token is a personal access token for GitHub.
// This allows access to a private repository.
Token string
}
// GitHubClient is a real GitHubAPI implementation.
type GitHubClient struct {
client *github.Client
}
var _ GitHubAPI = (*GitHubClient)(nil)
// NewGitHubClient returns a real GitHubClient instance.
func NewGitHubClient(config GitHubConfig) (*GitHubClient, error) {
var hc *http.Client
if len(config.Token) != 0 {
hc = newOAuth2Client(config.Token)
}
c := github.NewClient(hc)
if len(config.BaseURL) != 0 {
baseURL, err := url.Parse(config.BaseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse github base url: %s", err)
}
c.BaseURL = baseURL
}
return &GitHubClient{
client: c,
}, nil
}
// newOAuth2Client returns a *http.Client which sets a given token to the Authorization header.
// This allows access to a private repository.
func newOAuth2Client(token string) *http.Client {
t := &oauth2.Token{
AccessToken: token,
}
ts := oauth2.StaticTokenSource(t)
return oauth2.NewClient(context.Background(), ts)
}
// RepositoriesListReleases lists the releases for a repository.
func (c *GitHubClient) RepositoriesListReleases(ctx context.Context, owner, repo string, opt *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) {
return c.client.Repositories.ListReleases(ctx, owner, repo, opt)
}
// GitHubRelease is a release implementation which provides version information with GitHub Release.
type GitHubRelease struct {
// api is an instance of GitHubAPI interface.
// It can be replaced for testing.
api GitHubAPI
// owner is a namespace of repository.
owner string
// repo is a name of repository.
repo string
}
var _ Release = (*GitHubRelease)(nil)
// NewGitHubRelease is a factory method which returns an GitHubRelease instance.
func NewGitHubRelease(source string, config GitHubConfig) (Release, error) {
s := strings.SplitN(source, "/", 2)
if len(s) != 2 {
return nil, fmt.Errorf("failed to parse source: %s", source)
}
// If config.api is not set, create a default GitHubClient
var api GitHubAPI
if config.api == nil {
var err error
api, err = NewGitHubClient(config)
if err != nil {
return nil, err
}
} else {
api = config.api
}
return &GitHubRelease{
api: api,
owner: s[0],
repo: s[1],
}, nil
}
// ListReleases returns a list of unsorted all releases including pre-release.
func (r *GitHubRelease) ListReleases(ctx context.Context) ([]string, error) {
versions := []string{}
opt := &github.ListOptions{
PerPage: 100, // max
}
for {
releases, resp, err := r.api.RepositoriesListReleases(ctx, r.owner, r.repo, opt)
if err != nil {
return nil, fmt.Errorf("failed to list releases for %s/%s: %s", r.owner, r.repo, err)
}
for _, release := range releases {
v := tagNameToVersion(*release.TagName)
versions = append(versions, v)
}
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return versions, nil
}
0707010000002E000081A400000000000000000000000168975E36000013CE000000000000000000000000000000000000002600000000tfupdate-0.9.2/release/github_test.gopackage release
import (
"context"
"errors"
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-github/v28/github"
"golang.org/x/oauth2"
)
// mockGitHubClient is a mock GitHubAPI implementation.
type mockGitHubClient struct {
repositoryReleases []*github.RepositoryRelease
response *github.Response
err error
}
var _ GitHubAPI = (*mockGitHubClient)(nil)
func (c *mockGitHubClient) RepositoriesListReleases(ctx context.Context, owner, repo string, opt *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) { // nolint revive unused-parameter
return c.repositoryReleases, c.response, c.err
}
func TestNewGitHubClient(t *testing.T) {
cases := []struct {
baseURL string
want string
ok bool
}{
{
baseURL: "",
want: "https://api.github.com/",
ok: true,
},
{
baseURL: "https://api.github.com/",
want: "https://api.github.com/",
ok: true,
},
{
baseURL: "http://localhost/",
want: "http://localhost/",
ok: true,
},
{
baseURL: `https://api\.github.om/`,
want: "",
ok: false,
},
}
for _, tc := range cases {
config := GitHubConfig{
BaseURL: tc.baseURL,
}
got, err := NewGitHubClient(config)
if tc.ok && err != nil {
t.Errorf("NewGitHubClient() with baseURL = %s returns unexpected err: %s", tc.baseURL, err)
}
if !tc.ok && err == nil {
t.Errorf("NewGitHubClient() 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("NewGitHubClient() with baseURL = %s returns %s, but want %s", tc.baseURL, got.client.BaseURL.String(), tc.want)
}
}
}
}
func TestNewOAuth2Client(t *testing.T) {
cases := []struct {
token string
}{
{
token: "hoge",
},
}
for _, tc := range cases {
c := newOAuth2Client(tc.token)
trans := c.Transport.(*oauth2.Transport)
got, err := trans.Source.Token()
if err != nil {
t.Fatalf("failed to get a token from OAuth2 client: %s", err)
}
if got.AccessToken != tc.token {
t.Errorf("newOAuth2Client() expects to set a token = %s, but got = %s", tc.token, got.AccessToken)
}
}
}
func TestNewGitHubRelease(t *testing.T) {
cases := []struct {
source string
api GitHubAPI
owner string
repo string
ok bool
}{
{
source: "hoge/fuga",
api: &mockGitHubClient{},
owner: "hoge",
repo: "fuga",
ok: true,
},
{
source: "hoge",
api: &mockGitHubClient{},
owner: "",
repo: "",
ok: false,
},
}
for _, tc := range cases {
config := GitHubConfig{
api: tc.api,
}
got, err := NewGitHubRelease(tc.source, config)
if tc.ok && err != nil {
t.Errorf("NewGitHubRelease() with source = %s, api = %#v returns unexpected err: %s", tc.source, tc.api, err)
}
if !tc.ok && err == nil {
t.Errorf("NewGitHubRelease() with source = %s, api = %#v expects to return an error, but no error", tc.source, tc.api)
}
if tc.ok {
r := got.(*GitHubRelease)
if r.api != tc.api {
t.Errorf("NewGitHubRelease() with source = %s, api = %#v sets api = %#v, but want %s", tc.source, tc.api, r.api, tc.api)
}
if !(r.owner == tc.owner && r.repo == tc.repo) {
t.Errorf("NewGitHubRelease() with source = %s, api = %#v returns (%s, %s), but want (%s, %s)", tc.source, tc.api, r.owner, r.repo, tc.owner, tc.repo)
}
}
}
}
func TestGitHubReleaseListReleases(t *testing.T) {
tagv := []string{"v0.3.0", "v0.2.0", "v0.1.0"}
cases := []struct {
client *mockGitHubClient
want []string
ok bool
}{
{
client: &mockGitHubClient{
repositoryReleases: []*github.RepositoryRelease{
{TagName: &tagv[0]},
{TagName: &tagv[1]},
{TagName: &tagv[2]},
},
response: &github.Response{},
err: nil,
},
want: []string{"0.3.0", "0.2.0", "0.1.0"},
ok: true,
},
{
client: &mockGitHubClient{
repositoryReleases: nil,
response: &github.Response{},
// Actual error response type is *github.ErrorResponse,
// but we are not interested in the internal structure.
err: errors.New(`GET https://api.github.com/repos/hoge/fuga/releases: 404 Not Found []`),
},
want: nil,
ok: false,
},
}
source := "hoge/fuga"
for _, tc := range cases {
// Set a mock client
config := GitHubConfig{
api: tc.client,
}
r, err := NewGitHubRelease(source, config)
if err != nil {
t.Fatalf("failed to NewGitHubRelease(%s, %#v): %s", source, config, err)
}
got, err := r.ListReleases(context.Background())
if tc.ok && err != nil {
t.Errorf("(*GitHubRelease).ListReleases() with r = %s returns unexpected err: %+v", spew.Sdump(r), err)
}
if !tc.ok && err == nil {
t.Errorf("(*GitHubRelease).ListReleases() with r = %s expects to return an error, but no error", spew.Sdump(r))
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("(*GitHubRelease).ListReleases() with r = %s returns %s, but want = %s", spew.Sdump(r), got, tc.want)
}
}
}
0707010000002F000081A400000000000000000000000168975E3600000E6F000000000000000000000000000000000000002100000000tfupdate-0.9.2/release/gitlab.gopackage release
import (
"context"
"fmt"
"net/url"
"strings"
"github.com/xanzy/go-gitlab"
)
// GitLabAPI is an interface which calls GitLab API.
// This abstraction layer is needed for testing with mock.
type GitLabAPI interface {
// ProjectListReleases gets a pagenated of releases accessible by the authenticated user.
ProjectListReleases(ctx context.Context, owner, project string, opt *gitlab.ListReleasesOptions) ([]*gitlab.Release, *gitlab.Response, error)
}
// GitLabConfig is a set of configurations for GitLabRelease..
type GitLabConfig struct {
// api is an instance of GitLabAPI interface.
// It can be replaced for testing.
api GitLabAPI
// BaseURL is a URL for GitLab API requests.
// Defaults to the public GitLab API.
// BaseURL should always be specified with a trailing slash.
BaseURL string
// Token is a personal access token for GitLab, needed to use the api.
Token string
}
// GitLabClient is a real GitLabAPI implementation.
type GitLabClient struct {
client *gitlab.Client
}
var _ GitLabAPI = (*GitLabClient)(nil)
// NewGitLabClient returns a real GitLab instance.
func NewGitLabClient(config GitLabConfig) (*GitLabClient, error) {
if len(config.Token) == 0 {
return nil, fmt.Errorf("failed to get personal access token (env: GITLAB_TOKEN)")
}
c := gitlab.NewClient(nil, config.Token)
if len(config.BaseURL) != 0 {
baseURL, err := url.Parse(config.BaseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse gitlab base url: %s", err)
}
if err = c.SetBaseURL(baseURL.String()); err != nil {
return nil, err
}
}
return &GitLabClient{
client: c,
}, nil
}
// ProjectListReleases gets a pagenated of releases accessible by the authenticated user.
func (c *GitLabClient) ProjectListReleases(ctx context.Context, owner, project string, opt *gitlab.ListReleasesOptions) ([]*gitlab.Release, *gitlab.Response, error) {
return c.client.Releases.ListReleases(owner+"/"+project, opt, gitlab.WithContext(ctx))
}
// GitLabRelease is a release implementation which provides version information with GitLab Release.
type GitLabRelease struct {
// api is an instance of GitLabAPI interface.
// It can be replaced for testing.
api GitLabAPI
// owner is a namespace of project.
// limited to one level (group or personal - not sub-groups?)
owner string
// project is a name of project (repository).
project string
}
var _ Release = (*GitLabRelease)(nil)
// NewGitLabRelease is a factory method which returns an GitLabRelease instance.
func NewGitLabRelease(source string, config GitLabConfig) (*GitLabRelease, error) {
s := strings.SplitN(source, "/", 2)
if len(s) != 2 {
return nil, fmt.Errorf("failed to parse source: %s", source)
}
// If config.api is not set, create a default GitLabClient
var api GitLabAPI
if config.api == nil {
var err error
api, err = NewGitLabClient(config)
if err != nil {
return nil, err
}
} else {
api = config.api
}
return &GitLabRelease{
api: api,
owner: s[0],
project: s[1],
}, nil
}
// ListReleases returns a list of unsorted all releases including pre-release.
func (r *GitLabRelease) ListReleases(ctx context.Context) ([]string, error) {
versions := []string{}
opt := &gitlab.ListReleasesOptions{
PerPage: 100, // max
}
for {
releases, resp, err := r.api.ProjectListReleases(ctx, r.owner, r.project, opt)
if err != nil {
return nil, fmt.Errorf("failed to list releases for %s/%s: %s", r.owner, r.project, err)
}
for _, release := range releases {
v := tagNameToVersion(release.TagName)
versions = append(versions, v)
}
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return versions, nil
}
07070100000030000081A400000000000000000000000168975E3600001440000000000000000000000000000000000000002600000000tfupdate-0.9.2/release/gitlab_test.gopackage release
import (
"context"
"errors"
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/xanzy/go-gitlab"
)
// mockGitLabClient is a mock GitLabAPI implementation.
type mockGitLabClient struct {
projectReleases []*gitlab.Release
response *gitlab.Response
err error
}
var _ GitLabAPI = (*mockGitLabClient)(nil)
// ProjectListReleases returns a list of releases for the mockGitLabClient.
func (c *mockGitLabClient) ProjectListReleases(ctx context.Context, owner, repo string, opt *gitlab.ListReleasesOptions) ([]*gitlab.Release, *gitlab.Response, error) { // nolint revive unused-parameter
return c.projectReleases, c.response, c.err
}
// Test of NewGitLabClient(config GitLabConfig)
func TestNewGitLabClient(t *testing.T) {
cases := []struct {
baseURL string
want string
ok bool
}{ // test default value
{
baseURL: "",
want: "https://gitlab.com/api/v4/",
ok: true,
},
// test custom value
{
baseURL: "https://gitlab.com/api/v4/",
want: "https://gitlab.com/api/v4/",
ok: true,
},
// test custom value
{
baseURL: "http://localhost/api/v4/",
want: "http://localhost/api/v4/",
ok: true,
},
// test unparsable URL
{
baseURL: `https://gitlab\.com/api/v4/`,
want: "",
ok: false,
},
}
for _, tc := range cases {
config := GitLabConfig{
BaseURL: tc.baseURL,
Token: "dummy_token",
}
got, err := NewGitLabClient(config)
if tc.ok && err != nil {
t.Errorf("NewGitLabClient() with baseURL = %s returns unexpected err: %s", tc.baseURL, err)
}
if !tc.ok && err == nil {
t.Errorf("NewGitLabClient() 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("NewGitLabClient() with baseURL = %s returns %s, but want %s", tc.baseURL, got.client.BaseURL().String(), tc.want)
}
}
}
}
// Test of NewGitLabRelease(source string, config GitLabConfig)
func TestNewGitLabRelease(t *testing.T) {
cases := []struct {
source string
api GitLabAPI
owner string
project string
ok bool
}{ // test complete config
{
source: "gitlab-org/gitlab",
api: &mockGitLabClient{},
owner: "gitlab-org",
project: "gitlab",
ok: true,
},
// test release without owner or project
{
source: "gitlab",
api: &mockGitLabClient{},
owner: "",
project: "",
ok: false,
},
// test release with missing api
{
source: "gitlab-org/gitlab",
api: nil,
owner: "gitlab-org",
project: "gitlab",
ok: false,
},
}
for _, tc := range cases {
config := GitLabConfig{
api: tc.api,
}
got, err := NewGitLabRelease(tc.source, config)
if tc.ok && err != nil {
t.Errorf("NewGitLabRelease() with source = %s, api = %#v returns unexpected err: %s", tc.source, tc.api, err)
}
if !tc.ok && err == nil {
t.Errorf("NewGitLabRelease() with source = %s, api = %#v expects to return an error, but no error", tc.source, tc.api)
}
if tc.ok {
r := got
if r.api != tc.api {
t.Errorf("NewGitLabRelease() with source = %s, api = %#v sets api = %#v, but want %s", tc.source, tc.api, r.api, tc.api)
}
if !(r.owner == tc.owner && r.project == tc.project) {
t.Errorf("NewGitLabRelease() with source = %s, api = %#v returns (%s, %s), but want (%s, %s)", tc.source, tc.api, r.owner, r.project, tc.owner, tc.project)
}
}
}
}
// Test of GitLabRelease.List(ctx context.Context, maxLength int)
func TestGitLabReleaseListReleases(t *testing.T) {
tagv := []string{"v0.3.0", "v0.2.0", "v0.1.0"}
cases := []struct {
client *mockGitLabClient
want []string
ok bool
}{
{
client: &mockGitLabClient{
projectReleases: []*gitlab.Release{
{TagName: tagv[0]},
{TagName: tagv[1]},
{TagName: tagv[2]},
},
response: &gitlab.Response{},
err: nil,
},
want: []string{"0.3.0", "0.2.0", "0.1.0"},
ok: true,
},
{
client: &mockGitLabClient{
projectReleases: nil,
response: &gitlab.Response{},
// Actual error response type is *gitlab.ErrorResponse,
// but we are not interested in the internal structure.
err: errors.New(`GET https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab/releases: 404 Not Found []`),
},
want: nil,
ok: false,
},
}
source := "gitlab-org/gitlab"
for _, tc := range cases {
// Set a mock client
config := GitLabConfig{
api: tc.client,
}
r, err := NewGitLabRelease(source, config)
if err != nil {
t.Fatalf("failed to NewGitLabRelease(%s, %#v): %s", source, config, err)
}
got, err := r.ListReleases(context.Background())
if tc.ok && err != nil {
t.Errorf("(*GitLabRelease).ListReleases() with r = %s returns unexpected err: %+v", spew.Sdump(r), err)
}
if !tc.ok && err == nil {
t.Errorf("(*GitLabRelease).ListReleases() with r = %s expects to return an error, but no error", spew.Sdump(r))
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("(*GitLabRelease).ListReleases() with r = %s returns %s, but want = %s", spew.Sdump(r), got, tc.want)
}
}
}
07070100000031000081A400000000000000000000000168975E36000005B6000000000000000000000000000000000000002200000000tfupdate-0.9.2/release/release.gopackage release
import (
"context"
"errors"
)
// Release is an interface which provides version information of a module or provider.
type Release interface {
// ListReleases returns a list of unsorted all releases including pre-release.
ListReleases(ctx context.Context) ([]string, error)
}
// Latest returns the latest release.
// Note that GetLatestRelease API in GitHub and GitLab returns the most recent
// release, which doesn't means the latest stable release. I'm not sure it also
// affects Terraform Registry but I think we should use the same strategy for
// consistency. So we sort versions in semver order and find the latest non
// pre-release.
func Latest(ctx context.Context, r Release) (string, error) {
versions, err := List(ctx, r, 1, false)
if err != nil {
return "", err
}
if len(versions) == 0 {
return "", errors.New("no releases found")
}
return versions[0], nil
}
// List returns a list of releases in semver order.
// If preRelease is set to false, the result doesn't contain pre-releases.
func List(ctx context.Context, r Release, maxLength int, preRelease bool) ([]string, error) {
res, err := r.ListReleases(ctx)
if err != nil {
return nil, err
}
versions := toVersions(res)
sorted := sortVersions(versions)
rels := sorted
if !preRelease {
rels = excludePreReleases(sorted)
}
releases := fromVersions(rels)
start := len(releases) - minInt(maxLength, len(releases))
return releases[start:], nil
}
07070100000032000081A400000000000000000000000168975E3600000EEE000000000000000000000000000000000000002700000000tfupdate-0.9.2/release/release_test.gopackage release
import (
"context"
"errors"
"reflect"
"testing"
)
type mockRelease struct {
versions []string
err error
}
var _ Release = (*mockRelease)(nil)
func (r *mockRelease) ListReleases(ctx context.Context) ([]string, error) { // nolint revive unused-parameter
return r.versions, r.err
}
func TestLatest(t *testing.T) {
cases := []struct {
desc string
r Release
want string
ok bool
}{
{
desc: "sort",
r: &mockRelease{
versions: []string{"0.3.0", "0.2.0", "0.1.0", "0.1.1"},
err: nil,
},
want: "0.3.0",
ok: true,
},
{
desc: "pre-release",
r: &mockRelease{
versions: []string{"0.1.0", "0.2.0", "0.1.1", "0.3.0-beta1", "0.3.0-beta2", "0.3.0-alpha1", "0.3.0-rc"},
err: nil,
},
want: "0.2.0",
ok: true,
},
{
desc: "no release",
r: &mockRelease{
versions: []string{},
err: nil,
},
want: "",
ok: false,
},
{
desc: "api error",
r: &mockRelease{
versions: nil,
err: errors.New("mocked error"),
},
want: "",
ok: false,
},
{
desc: "parse error",
r: &mockRelease{
versions: []string{"foo", "0.3.0", "0.2.0", "0.1.0", "0.1.1"},
err: nil,
},
want: "0.3.0",
ok: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := Latest(context.Background(), tc.r)
if tc.ok && err != nil {
t.Fatalf("unexpected err: %#v", err)
}
if !tc.ok && err == nil {
t.Fatalf("expects to return an error, but no error. got = %#v", got)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got = %#v, but want = %#v", got, tc.want)
}
})
}
}
func TestList(t *testing.T) {
cases := []struct {
desc string
r Release
maxLength int
preRelease bool
want []string
ok bool
}{
{
desc: "sort",
r: &mockRelease{
versions: []string{"0.3.0", "0.2.0", "0.1.0", "0.1.1"},
err: nil,
},
maxLength: 5,
preRelease: true,
want: []string{"0.1.0", "0.1.1", "0.2.0", "0.3.0"},
ok: true,
},
{
desc: "maxLength",
r: &mockRelease{
versions: []string{"0.3.0", "0.2.0", "0.1.0", "0.1.1"},
err: nil,
},
maxLength: 3,
preRelease: true,
want: []string{"0.1.1", "0.2.0", "0.3.0"},
ok: true,
},
{
desc: "include pre-release",
r: &mockRelease{
versions: []string{"0.3.0", "0.2.0", "0.1.0", "0.1.1", "0.3.0-beta1", "0.3.0-beta2", "0.3.0-alpha1", "0.3.0-rc"},
err: nil,
},
maxLength: 3,
preRelease: true,
want: []string{"0.3.0-beta2", "0.3.0-rc", "0.3.0"},
ok: true,
},
{
desc: "exclude pre-release",
r: &mockRelease{
versions: []string{"0.3.0", "0.2.0", "0.1.0", "0.1.1", "0.3.0-beta1", "0.3.0-beta2", "0.3.0-alpha1", "0.3.0-rc"},
err: nil,
},
maxLength: 3,
preRelease: false,
want: []string{"0.1.1", "0.2.0", "0.3.0"},
ok: true,
},
{
desc: "empty",
r: &mockRelease{
versions: []string{},
err: nil,
},
maxLength: 3,
preRelease: true,
want: []string{},
ok: true,
},
{
desc: "api error",
r: &mockRelease{
versions: nil,
err: errors.New("mocked error"),
},
maxLength: 3,
preRelease: true,
want: nil,
ok: false,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := List(context.Background(), tc.r, tc.maxLength, tc.preRelease)
if tc.ok && err != nil {
t.Fatalf("unexpected err: %#v", err)
}
if !tc.ok && err == nil {
t.Fatalf("expects to return an error, but no error. got = %#v", got)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got = %#v, but want = %#v", got, tc.want)
}
})
}
}
07070100000033000081A400000000000000000000000168975E3600000D9A000000000000000000000000000000000000002500000000tfupdate-0.9.2/release/tfregistry.gopackage release
import (
"context"
"fmt"
"strings"
"github.com/minamijoyo/tfupdate/tfregistry"
)
// TFRegistryModuleRelease is a release implementation which provides version information with TFRegistryModule Release.
type TFRegistryModuleRelease struct {
// api is an instance of tfregistry.API interface.
// It can be replaced for testing.
api tfregistry.API
// namespace is a user name which owns the module.
namespace string
// name is a name of the module.
name string
// provider is a name of the provider.
provider string
}
var _ Release = (*TFRegistryModuleRelease)(nil)
// NewTFRegistryModuleRelease is a factory method which returns an TFRegistryModuleRelease instance.
func NewTFRegistryModuleRelease(source string, config tfregistry.Config) (Release, error) {
s := strings.SplitN(source, "/", 3)
if len(s) != 3 {
return nil, fmt.Errorf("failed to parse source: %s", source)
}
client, err := tfregistry.NewClient(config)
if err != nil {
return nil, err
}
return &TFRegistryModuleRelease{
api: client,
namespace: s[0],
name: s[1],
provider: s[2],
}, nil
}
// ListReleases returns a list of unsorted all releases including pre-release.
func (r *TFRegistryModuleRelease) ListReleases(ctx context.Context) ([]string, error) {
req := &tfregistry.ListModuleVersionsRequest{
Namespace: r.namespace,
Name: r.name,
Provider: r.provider,
}
response, err := r.api.ListModuleVersions(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to get a list of versions for %s/%s/%s: %s", r.namespace, r.name, r.provider, err)
}
// Extract versions from the response
if len(response.Modules) == 0 {
return []string{}, nil
}
versions := []string{}
for _, version := range response.Modules[0].Versions {
versions = append(versions, version.Version)
}
return versions, nil
}
// TFRegistryProviderRelease is a release implementation which provides version information with TFRegistryProvider Release.
type TFRegistryProviderRelease struct {
// api is an instance of tfregistry.API interface.
// It can be replaced for testing.
api tfregistry.API
// The user or organization the provider is owned by.
namespace string
// The type name of the provider.
providerType string
}
var _ Release = (*TFRegistryProviderRelease)(nil)
// NewTFRegistryProviderRelease is a factory method which returns an TFRegistryProviderRelease instance.
func NewTFRegistryProviderRelease(source string, config tfregistry.Config) (Release, error) {
s := strings.SplitN(source, "/", 2)
if len(s) != 2 {
return nil, fmt.Errorf("failed to parse source: %s", source)
}
client, err := tfregistry.NewClient(config)
if err != nil {
return nil, err
}
return &TFRegistryProviderRelease{
api: client,
namespace: s[0],
providerType: s[1],
}, nil
}
// ListReleases returns a list of unsorted all releases including pre-release.
func (r *TFRegistryProviderRelease) ListReleases(ctx context.Context) ([]string, error) {
req := &tfregistry.ListProviderVersionsRequest{
Namespace: r.namespace,
Type: r.providerType,
}
response, err := r.api.ListProviderVersions(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to get a list of versions for %s/%s: %s", r.namespace, r.providerType, err)
}
// Extract versions from the response
versions := []string{}
for _, version := range response.Versions {
versions = append(versions, version.Version)
}
return versions, nil
}
07070100000034000081A400000000000000000000000168975E36000018D6000000000000000000000000000000000000002A00000000tfupdate-0.9.2/release/tfregistry_test.gopackage release
import (
"context"
"errors"
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/minamijoyo/tfupdate/tfregistry"
)
// mockTFRegistryClient is a mock implementation of tfregistry.API
type mockTFRegistryClient struct {
moduleRes *tfregistry.ListModuleVersionsResponse
providerRes *tfregistry.ListProviderVersionsResponse
err error
}
var _ tfregistry.API = (*mockTFRegistryClient)(nil)
func (c *mockTFRegistryClient) ListModuleVersions(_ context.Context, _ *tfregistry.ListModuleVersionsRequest) (*tfregistry.ListModuleVersionsResponse, error) {
return c.moduleRes, c.err
}
func (c *mockTFRegistryClient) ListProviderVersions(_ context.Context, _ *tfregistry.ListProviderVersionsRequest) (*tfregistry.ListProviderVersionsResponse, error) {
return c.providerRes, c.err
}
func (c *mockTFRegistryClient) ProviderPackageMetadata(_ context.Context, _ *tfregistry.ProviderPackageMetadataRequest) (*tfregistry.ProviderPackageMetadataResponse, error) {
return nil, nil // dummy implementation as it's not used in tests
}
func TestNewTFRegistryModuleRelease(t *testing.T) {
cases := []struct {
source string
namespace string
name string
provider string
ok bool
}{
{
source: "hoge/fuga/piyo",
namespace: "hoge",
name: "fuga",
provider: "piyo",
ok: true,
},
{
source: "hoge",
namespace: "",
name: "",
provider: "",
ok: false,
},
}
for _, tc := range cases {
config := tfregistry.Config{}
got, err := NewTFRegistryModuleRelease(tc.source, config)
if tc.ok && err != nil {
t.Errorf("NewTFRegistryModuleRelease() with source = %s returns unexpected err: %s", tc.source, err)
}
if !tc.ok && err == nil {
t.Errorf("NewTFRegistryModuleRelease() with source = %s expects to return an error, but no error", tc.source)
}
if tc.ok {
r := got.(*TFRegistryModuleRelease)
if !(r.namespace == tc.namespace && r.name == tc.name && r.provider == tc.provider) {
t.Errorf("NewTFRegistryModuleRelease() with source = %s returns (%s, %s, %s), but want (%s, %s, %s)", tc.source, r.namespace, r.name, r.provider, tc.namespace, tc.name, tc.provider)
}
}
}
}
func TestNewTFRegistryProviderRelease(t *testing.T) {
cases := []struct {
source string
namespace string
providerType string
ok bool
}{
{
source: "hoge/piyo",
namespace: "hoge",
providerType: "piyo",
ok: true,
},
{
source: "hoge",
namespace: "",
providerType: "",
ok: false,
},
}
for _, tc := range cases {
config := tfregistry.Config{}
got, err := NewTFRegistryProviderRelease(tc.source, config)
if tc.ok && err != nil {
t.Errorf("NewTFRegistryProviderRelease() with source = %s returns unexpected err: %s", tc.source, err)
}
if !tc.ok && err == nil {
t.Errorf("NewTFRegistryProviderRelease() with source = %s expects to return an error, but no error", tc.source)
}
if tc.ok {
r := got.(*TFRegistryProviderRelease)
if !(r.namespace == tc.namespace && r.providerType == tc.providerType) {
t.Errorf("NewTFRegistryProviderRelease() with source = %s returns (%s, %s), but want (%s, %s)", tc.source, r.namespace, r.providerType, tc.namespace, tc.providerType)
}
}
}
}
func TestTFRegistryModuleReleaseListReleases(t *testing.T) {
cases := []struct {
client *mockTFRegistryClient
want []string
ok bool
}{
{
client: &mockTFRegistryClient{
moduleRes: &tfregistry.ListModuleVersionsResponse{
Modules: []tfregistry.ModuleVersions{
{
Versions: []tfregistry.ModuleVersion{
{Version: "0.3.0"},
{Version: "0.2.0"},
{Version: "0.1.0"},
},
},
},
},
err: nil,
},
want: []string{"0.3.0", "0.2.0", "0.1.0"},
ok: true,
},
{
client: &mockTFRegistryClient{
moduleRes: nil,
err: errors.New(`unexpected HTTP Status Code: 404`),
},
want: nil,
ok: false,
},
}
source := "hoge/fuga/piyo"
for _, tc := range cases {
config := tfregistry.Config{}
r, err := NewTFRegistryModuleRelease(source, config)
if err != nil {
t.Fatalf("failed to NewTFRegistryModuleRelease(%s, %#v): %s", source, config, err)
}
r.(*TFRegistryModuleRelease).api = tc.client
got, err := r.ListReleases(context.Background())
if tc.ok && err != nil {
t.Errorf("(*TFRegistryModuleRelease).ListReleases() with r = %s returns unexpected err: %+v", spew.Sdump(r), err)
}
if !tc.ok && err == nil {
t.Errorf("(*TFRegistryModuleRelease).ListReleases() with r = %s expects to return an error, but no error", spew.Sdump(r))
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("(*TFRegistryModuleRelease).ListReleases() with r = %s returns %s, but want = %s", spew.Sdump(r), got, tc.want)
}
}
}
func TestTFRegistryProviderReleaseListReleases(t *testing.T) {
cases := []struct {
client *mockTFRegistryClient
want []string
ok bool
}{
{
client: &mockTFRegistryClient{
providerRes: &tfregistry.ListProviderVersionsResponse{
Versions: []tfregistry.ProviderVersion{
{Version: "0.3.0"},
{Version: "0.2.0"},
{Version: "0.1.0"},
},
},
err: nil,
},
want: []string{"0.3.0", "0.2.0", "0.1.0"},
ok: true,
},
{
client: &mockTFRegistryClient{
providerRes: nil,
err: errors.New(`unexpected HTTP Status Code: 404`),
},
want: nil,
ok: false,
},
}
source := "hoge/piyo"
for _, tc := range cases {
config := tfregistry.Config{}
r, err := NewTFRegistryProviderRelease(source, config)
if err != nil {
t.Fatalf("failed to NewTFRegistryProviderRelease(%s, %#v): %s", source, config, err)
}
r.(*TFRegistryProviderRelease).api = tc.client
got, err := r.ListReleases(context.Background())
if tc.ok && err != nil {
t.Errorf("(*NewTFRegistryProviderRelease).ListReleases() with r = %s returns unexpected err: %+v", spew.Sdump(r), err)
}
if !tc.ok && err == nil {
t.Errorf("(*NewTFRegistryProviderRelease).ListReleases() with r = %s expects to return an error, but no error", spew.Sdump(r))
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("(*NewTFRegistryProviderRelease).ListReleases() with r = %s returns %s, but want = %s", spew.Sdump(r), got, tc.want)
}
}
}
07070100000035000081A400000000000000000000000168975E3600000639000000000000000000000000000000000000002200000000tfupdate-0.9.2/release/version.gopackage release
import (
"sort"
version "github.com/hashicorp/go-version"
)
func tagNameToVersion(tagName string) string {
// if a tagName starts with `v`, remove it.
if tagName[0] == 'v' {
return tagName[1:]
}
return tagName
}
func reverseStringSlice(s []string) []string {
r := []string{}
// apparently inefficient but simple way
for i := len(s) - 1; i >= 0; i-- {
r = append(r, s[i])
}
return r
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
// toVersions converts []string to []*version.Version.
// Ignore if parse error.
func toVersions(versionsRaw []string) []*version.Version {
versions := []*version.Version{}
for _, raw := range versionsRaw {
v, err := version.NewVersion(raw)
if err != nil {
continue
}
versions = append(versions, v)
}
return versions
}
// fromVersions converts []*version.Version to []string.
func fromVersions(versions []*version.Version) []string {
versionsRaw := make([]string, len(versions))
for i, v := range versions {
raw := v.String()
versionsRaw[i] = raw
}
return versionsRaw
}
// sortVersions sort a list of versions in semver order.
func sortVersions(versions []*version.Version) []*version.Version {
sort.Sort(version.Collection(versions))
return versions
}
// excludePreReleases excludes pre-releases such as alpha, beta, rc.
func excludePreReleases(versions []*version.Version) []*version.Version {
// exclude pre-release
filtered := []*version.Version{}
for _, v := range versions {
if len(v.Prerelease()) == 0 {
filtered = append(filtered, v)
}
}
return filtered
}
07070100000036000081A400000000000000000000000168975E36000007BB000000000000000000000000000000000000002700000000tfupdate-0.9.2/release/version_test.gopackage release
import (
"reflect"
"testing"
)
func TestSortVersions(t *testing.T) {
cases := []struct {
desc string
versionsRaw []string
want []string
}{
{
desc: "simple",
versionsRaw: []string{"0.3.0", "0.2.0", "0.1.0", "0.1.1"},
want: []string{"0.1.0", "0.1.1", "0.2.0", "0.3.0"},
},
{
desc: "empty",
versionsRaw: []string{},
want: []string{},
},
{
desc: "pre-release",
versionsRaw: []string{"0.3.0", "0.2.0", "0.1.0", "0.1.1", "0.3.0-beta1", "0.3.0-beta2", "0.3.0-alpha1", "0.3.0-rc"},
want: []string{"0.1.0", "0.1.1", "0.2.0", "0.3.0-alpha1", "0.3.0-beta1", "0.3.0-beta2", "0.3.0-rc", "0.3.0"},
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := fromVersions(sortVersions(toVersions(tc.versionsRaw)))
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got = %#v, but want = %#v", got, tc.want)
}
})
}
}
func TestExcludePreReleases(t *testing.T) {
cases := []struct {
desc string
versionsRaw []string
want []string
}{
{
desc: "simple",
versionsRaw: []string{"0.1.0", "0.1.1", "0.2.0", "0.3.0-alpha1", "0.3.0-beta1", "0.3.0-beta2", "0.3.0-rc", "0.3.0"},
want: []string{"0.1.0", "0.1.1", "0.2.0", "0.3.0"},
},
{
desc: "no pre-relases",
versionsRaw: []string{"0.1.0", "0.1.1", "0.2.0", "0.3.0"},
want: []string{"0.1.0", "0.1.1", "0.2.0", "0.3.0"},
},
{
desc: "no stable relases",
versionsRaw: []string{"0.3.0-alpha1", "0.3.0-beta1", "0.3.0-beta2", "0.3.0-rc"},
want: []string{},
},
{
desc: "empty",
versionsRaw: []string{},
want: []string{},
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := fromVersions(excludePreReleases(toVersions(tc.versionsRaw)))
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got = %#v, but want = %#v", got, tc.want)
}
})
}
}
07070100000037000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001700000000tfupdate-0.9.2/scripts07070100000038000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001F00000000tfupdate-0.9.2/scripts/testacc07070100000039000081ED00000000000000000000000168975E36000001AE000000000000000000000000000000000000002600000000tfupdate-0.9.2/scripts/testacc/all.sh#!/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
0707010000003A000081ED00000000000000000000000168975E3600000794000000000000000000000000000000000000002700000000tfupdate-0.9.2/scripts/testacc/lock.sh#!/bin/bash
set -eo pipefail
usage()
{
cat << EOF
Usage: $(basename "$0") <command> <fixture>
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
"$TFUPDATE_EXEC_PATH" 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
"$TFUPDATE_EXEC_PATH" 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
0707010000003B000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001D00000000tfupdate-0.9.2/test-fixtures0707010000003C000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000002200000000tfupdate-0.9.2/test-fixtures/lock0707010000003D000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000002900000000tfupdate-0.9.2/test-fixtures/lock/simple0707010000003E000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000002E00000000tfupdate-0.9.2/test-fixtures/lock/simple/dir10707010000003F000081A400000000000000000000000168975E3600000075000000000000000000000000000000000000003600000000tfupdate-0.9.2/test-fixtures/lock/simple/dir1/main.tfterraform {
required_providers {
null = {
source = "hashicorp/null"
version = "3.1.1"
}
}
}
07070100000040000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000002E00000000tfupdate-0.9.2/test-fixtures/lock/simple/dir207070100000041000081A400000000000000000000000168975E3600000075000000000000000000000000000000000000003600000000tfupdate-0.9.2/test-fixtures/lock/simple/dir2/main.tfterraform {
required_providers {
null = {
source = "hashicorp/null"
version = "3.1.1"
}
}
}
07070100000042000081A400000000000000000000000168975E3600000075000000000000000000000000000000000000003100000000tfupdate-0.9.2/test-fixtures/lock/simple/main.tfterraform {
required_providers {
null = {
source = "hashicorp/null"
version = "3.1.1"
}
}
}
07070100000043000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001A00000000tfupdate-0.9.2/tfregistry07070100000044000081A400000000000000000000000168975E3600000C33000000000000000000000000000000000000002400000000tfupdate-0.9.2/tfregistry/client.gopackage tfregistry
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
)
// To avoid depending on a specific version of Terraform,
// we implement a pure Terraform Registry API client.
// https://www.terraform.io/docs/registry/api.html
//
// The public Terraform and OpenTofu registries are supported.
// There are other APIs and request/response fields,
// but we define only the ones we need here to keep it simple.
const (
// The public Terraform Registry API endpoint.
defaultBaseURL = "https://registry.terraform.io/"
)
// API is an interface which calls Terraform Registry API.
// This works for both Terraform and OpenTofu registries.
// This abstraction layer is needed for testing with mock.
type API interface {
ModuleV1API
ProviderV1API
}
// Config is a set of configurations for TFRegistry client.
type Config struct {
// HTTPClient is a http client which communicates with the API.
// If nil, a default client will be used.
HTTPClient *http.Client
// BaseURL is a URL for Terraform Registry API requests.
// Defaults to the public Terraform Registry API.
// We have not yet implemented registry authentication,
// so private registries such as HCP Terraform are not yet supported.
// BaseURL should always be specified with a trailing slash.
BaseURL string
}
// Client manages communication with the Terraform Registry API.
type Client struct {
// httpClient is a http client which communicates with the API.
httpClient *http.Client
// BaseURL is a base url for API requests. Defaults to the public Terraform Registry API.
BaseURL *url.URL
}
// Ensure Client implements API interface
var _ API = (*Client)(nil)
// NewClient returns a new Client instance.
func NewClient(config Config) (*Client, error) {
httpClient := config.HTTPClient
if httpClient == nil {
httpClient = &http.Client{}
}
var baseURL *url.URL
var err error
if config.BaseURL != "" {
baseURL, err = url.Parse(config.BaseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse base URL: %s", err)
}
} else {
baseURL, _ = url.Parse(defaultBaseURL)
}
c := &Client{httpClient: httpClient, BaseURL: baseURL}
return c, nil
}
// newRequest builds a http Request instance.
func (c *Client) newRequest(ctx context.Context, method string, subPath string, body io.Reader) (*http.Request, error) {
endpointURL := *c.BaseURL
endpointURL.Path = path.Join(c.BaseURL.Path, subPath)
req, err := http.NewRequest(method, endpointURL.String(), body)
if err != nil {
return nil, fmt.Errorf("failed to build HTTP request: err = %s, method = %s, endpointURL = %s, body = %#v", err, method, endpointURL.String(), body)
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
return req, nil
}
// decodeBody decodes a raw body data into a specific response type structure.
func decodeBody(resp *http.Response, out interface{}) error {
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
err := decoder.Decode(out)
if err != nil {
return fmt.Errorf("failed to decode response: err = %s, resp = %#v", err, resp)
}
return nil
}
07070100000045000081A400000000000000000000000168975E360000043C000000000000000000000000000000000000002900000000tfupdate-0.9.2/tfregistry/client_test.gopackage tfregistry
import "testing"
func TestNewClient(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 := Config{
BaseURL: tc.baseURL,
}
got, err := NewClient(config)
if tc.ok && err != nil {
t.Errorf("NewClient() with baseURL = %s returns unexpected err: %s", tc.baseURL, err)
}
if !tc.ok && err == nil {
t.Errorf("NewClient() with baseURL = %s expects to return an error, but no error", tc.baseURL)
}
if tc.ok {
if got.BaseURL.String() != tc.want {
t.Errorf("NewClient() with baseURL = %s returns %s, but want %s", tc.baseURL, got.BaseURL.String(), tc.want)
}
}
}
}
07070100000046000081A400000000000000000000000168975E360000021B000000000000000000000000000000000000002200000000tfupdate-0.9.2/tfregistry/mock.gopackage tfregistry
import (
"net/http"
"net/http/httptest"
"net/url"
)
// 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) *Client {
config := Config{
HTTPClient: &http.Client{},
}
c, _ := NewClient(config)
c.BaseURL = mockServerURL
return c
}
07070100000047000081A400000000000000000000000168975E360000041C000000000000000000000000000000000000002400000000tfupdate-0.9.2/tfregistry/module.gopackage tfregistry
import (
"context"
)
const (
// moduleV1Service is a sub path of module v1 service endpoint.
// The service discovery protocol is not implemented for now.
// https://www.terraform.io/docs/internals/remote-service-discovery.html
//
// Include slashes for later implementation of service discovery.
// curl https://registry.terraform.io/.well-known/terraform.json
// {"modules.v1":"/v1/modules/","providers.v1":"/v1/providers/"}
moduleV1Service = "/v1/modules/"
)
// ModuleV1API is an interface for the module v1 service.
type ModuleV1API interface {
// ListModuleVersions returns all versions of a module for a single provider.
// This works for both Terraform and OpenTofu registries.
// https://developer.hashicorp.com/terraform/registry/api-docs#list-available-versions-for-a-specific-module
// https://opentofu.org/docs/internals/module-registry-protocol/#list-available-versions-for-a-specific-module
ListModuleVersions(ctx context.Context, req *ListModuleVersionsRequest) (*ListModuleVersionsResponse, error)
}
07070100000048000081A400000000000000000000000168975E36000008B7000000000000000000000000000000000000002D00000000tfupdate-0.9.2/tfregistry/module_versions.gopackage tfregistry
import (
"context"
"fmt"
)
// ListModuleVersionsRequest is a request parameter for the ListModuleVersions API.
type ListModuleVersionsRequest struct {
// The user or organization the module is owned by.
Namespace string `json:"namespace"`
// The name of the module.
Name string `json:"name"`
// The name of the provider.
Provider string `json:"provider"`
}
// ListModuleVersionsResponse is a response data for the ListModuleVersions API.
type ListModuleVersionsResponse struct {
// Modules is an array containing module information.
// The first element contains the requested module.
Modules []ModuleVersions `json:"modules"`
}
// ModuleVersions represents version information for a module.
type ModuleVersions struct {
// Versions is a list of available versions.
Versions []ModuleVersion `json:"versions"`
}
// ModuleVersion represents a single version of a module.
type ModuleVersion struct {
// Version is the version string.
Version string `json:"version"`
}
// ListModuleVersions returns all versions of a module for a single provider.
// This works for both Terraform and OpenTofu registries.
func (c *Client) ListModuleVersions(ctx context.Context, req *ListModuleVersionsRequest) (*ListModuleVersionsResponse, 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/versions", 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 ListModuleVersionsResponse
if err := decodeBody(httpResponse, &res); err != nil {
return nil, err
}
return &res, nil
}
07070100000049000081A400000000000000000000000168975E3600000997000000000000000000000000000000000000003200000000tfupdate-0.9.2/tfregistry/module_versions_test.gopackage tfregistry
import (
"context"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestListModuleVersions(t *testing.T) {
cases := []struct {
desc string
req *ListModuleVersionsRequest
ok bool
code int
res string
want *ListModuleVersionsResponse
}{
{
desc: "simple",
req: &ListModuleVersionsRequest{
Namespace: "terraform-aws-modules",
Name: "vpc",
Provider: "aws",
},
ok: true,
code: 200,
res: `{"modules": [{"versions": [{"version": "2.22.0"}, {"version": "2.23.0"}, {"version": "2.24.0"}]}]}`,
want: &ListModuleVersionsResponse{
Modules: []ModuleVersions{
{
Versions: []ModuleVersion{
{Version: "2.22.0"},
{Version: "2.23.0"},
{Version: "2.24.0"},
},
},
},
},
},
{
desc: "not found",
req: &ListModuleVersionsRequest{
Namespace: "hoge",
Name: "fuga",
Provider: "piyo",
},
ok: false,
code: 404,
res: `{"errors":["Not Found"]}`,
want: nil,
},
{
desc: "invalid request (Namespace)",
req: &ListModuleVersionsRequest{
Namespace: "",
Name: "fuga",
Provider: "piyo",
},
ok: false,
code: 0,
res: "",
want: nil,
},
{
desc: "invalid request (Name)",
req: &ListModuleVersionsRequest{
Namespace: "hoge",
Name: "",
Provider: "piyo",
},
ok: false,
code: 0,
res: "",
want: nil,
},
{
desc: "invalid request (Provider)",
req: &ListModuleVersionsRequest{
Namespace: "hoge",
Name: "fuga",
Provider: "",
},
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/versions", moduleV1Service, tc.req.Namespace, tc.req.Name, tc.req.Provider)
mux.HandleFunc(subPath, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(tc.code)
fmt.Fprint(w, tc.res)
})
got, err := client.ListModuleVersions(context.Background(), tc.req)
if tc.ok && err != nil {
t.Fatalf("failed to call ListModuleVersions: 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)
}
})
}
}
0707010000004A000081A400000000000000000000000168975E36000005A6000000000000000000000000000000000000002600000000tfupdate-0.9.2/tfregistry/provider.gopackage tfregistry
import (
"context"
)
const (
// providerV1Service is a sub path of provider v1 service endpoint.
// The service discovery protocol is not implemented for now.
// https://www.terraform.io/docs/internals/provider-registry-protocol.html#service-discovery
//
// Include slashes for later implementation of service discovery.
// curl https://registry.terraform.io/.well-known/terraform.json
// {"modules.v1":"/v1/modules/","providers.v1":"/v1/providers/"}
providerV1Service = "/v1/providers/"
)
// ProviderV1API is an interface for the provider v1 service.
type ProviderV1API interface {
// ListProviderVersions returns all versions of a provider.
// This works for both Terraform and OpenTofu registries.
// https://developer.hashicorp.com/terraform/internals/provider-registry-protocol#list-available-versions
// https://opentofu.org/docs/internals/provider-registry-protocol/#list-available-versions
ListProviderVersions(ctx context.Context, req *ListProviderVersionsRequest) (*ListProviderVersionsResponse, error)
// ProviderPackageMetadata returns a package metadata of a provider.
// https://developer.hashicorp.com/terraform/internals/provider-registry-protocol#find-a-provider-package
// https://opentofu.org/docs/internals/provider-registry-protocol/#find-a-provider-package
ProviderPackageMetadata(ctx context.Context, req *ProviderPackageMetadataRequest) (*ProviderPackageMetadataResponse, error)
}
0707010000004B000081A400000000000000000000000168975E3600000E04000000000000000000000000000000000000003700000000tfupdate-0.9.2/tfregistry/provider_package_metadata.gopackage 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
}
0707010000004C000081A400000000000000000000000168975E3600002FB6000000000000000000000000000000000000003C00000000tfupdate-0.9.2/tfregistry/provider_package_metadata_test.gopackage 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, _ *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"
}
]
}
}
`
0707010000004D000081A400000000000000000000000168975E36000008BB000000000000000000000000000000000000002F00000000tfupdate-0.9.2/tfregistry/provider_versions.gopackage tfregistry
import (
"context"
"fmt"
)
// ListProviderVersionsRequest is a request parameter for ListProviderVersions API.
type ListProviderVersionsRequest struct {
// The user or organization the provider is owned by.
Namespace string `json:"namespace"`
// The type name of the provider.
Type string `json:"type"`
}
// ListProviderVersionsResponse is a response data for ListProviderVersions API.
type ListProviderVersionsResponse struct {
// Versions is a list of available versions.
Versions []ProviderVersion `json:"versions"`
}
// ProviderVersion represents a single version of a provider.
type ProviderVersion struct {
// Version is the version string.
Version string `json:"version"`
// Protocols is a list of supported protocol versions.
Protocols []string `json:"protocols,omitempty"`
// Platforms is a list of supported platforms.
Platforms []ProviderPlatform `json:"platforms,omitempty"`
}
// ProviderPlatform represents a platform supported by a provider version.
type ProviderPlatform struct {
// OS is the operating system.
OS string `json:"os"`
// Arch is the architecture.
Arch string `json:"arch"`
}
// ListProviderVersions returns all versions of a provider.
// This works for both Terraform and OpenTofu registries.
func (c *Client) ListProviderVersions(ctx context.Context, req *ListProviderVersionsRequest) (*ListProviderVersionsResponse, 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/versions", 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 ListProviderVersionsResponse
if err := decodeBody(httpResponse, &res); err != nil {
return nil, err
}
return &res, nil
}
0707010000004E000081A400000000000000000000000168975E3600000A7E000000000000000000000000000000000000003400000000tfupdate-0.9.2/tfregistry/provider_versions_test.gopackage tfregistry
import (
"context"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestListProviderVersions(t *testing.T) {
cases := []struct {
desc string
req *ListProviderVersionsRequest
ok bool
code int
res string
want *ListProviderVersionsResponse
}{
{
desc: "simple",
req: &ListProviderVersionsRequest{
Namespace: "hashicorp",
Type: "aws",
},
ok: true,
code: 200,
res: `{"versions": [{"version": "3.5.0"}, {"version": "3.6.0"}, {"version": "3.7.0"}]}`,
want: &ListProviderVersionsResponse{
Versions: []ProviderVersion{
{Version: "3.5.0"},
{Version: "3.6.0"},
{Version: "3.7.0"},
},
},
},
{
desc: "with protocols and platforms",
req: &ListProviderVersionsRequest{
Namespace: "hashicorp",
Type: "aws",
},
ok: true,
code: 200,
res: `{"versions": [{"version": "3.7.0", "protocols": ["4.0", "5.1"], "platforms": [{"os": "linux", "arch": "amd64"}, {"os": "darwin", "arch": "amd64"}]}]}`,
want: &ListProviderVersionsResponse{
Versions: []ProviderVersion{
{
Version: "3.7.0",
Protocols: []string{"4.0", "5.1"},
Platforms: []ProviderPlatform{
{OS: "linux", Arch: "amd64"},
{OS: "darwin", Arch: "amd64"},
},
},
},
},
},
{
desc: "not found",
req: &ListProviderVersionsRequest{
Namespace: "hoge",
Type: "piyo",
},
ok: false,
code: 404,
res: `{"errors":["Not Found"]}`,
want: nil,
},
{
desc: "invalid request (Namespace)",
req: &ListProviderVersionsRequest{
Namespace: "",
Type: "piyo",
},
ok: false,
code: 0,
res: "",
want: nil,
},
{
desc: "invalid request (Type)",
req: &ListProviderVersionsRequest{
Namespace: "hoge",
Type: "",
},
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/versions", providerV1Service, tc.req.Namespace, tc.req.Type)
mux.HandleFunc(subPath, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(tc.code)
fmt.Fprint(w, tc.res)
})
got, err := client.ListProviderVersions(context.Background(), tc.req)
if tc.ok && err != nil {
t.Fatalf("failed to call ListProviderVersions: 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)
}
})
}
}
0707010000004F000041ED00000000000000000000000268975E3600000000000000000000000000000000000000000000001800000000tfupdate-0.9.2/tfupdate07070100000050000081A400000000000000000000000168975E3600001907000000000000000000000000000000000000002300000000tfupdate-0.9.2/tfupdate/context.gopackage tfupdate
import (
"log"
version "github.com/hashicorp/go-version"
"github.com/minamijoyo/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.
// Note that the standard io/fs.FS doesn't support any write operations and
// afero.IOFS doesn't support absolute paths at the time of writing.
// It might be better to use the native OS filesystem for testing without
// relying on afero.
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) {
requiredProviders := make(map[string]*tfconfig.ProviderRequirement)
m, diags := tfconfig.LoadModuleFromFilesystem(aferoToTfconfigFS(gc.fs), dir)
if diags.HasErrors() {
// There is a known issue passing absolute paths to afero.IOFS results in
// an error, but as the result of module inspection is not essential for
// all use cases now, we intentionally ignore the error here.
// https://github.com/minamijoyo/tfupdate/issues/93
log.Printf("[DEBUG] failed to load module: dir = %s, err = %s", dir, diags)
} else {
requiredProviders = m.RequiredProviders
}
c := &ModuleContext{
gc: gc,
dir: dir,
requiredProviders: 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 ""
}
// ResolveProviderShortNameFromSource is a helper function to resolve provider
// short names from the source address.
// If not found, return an empty string.
func (mc *ModuleContext) ResolveProviderShortNameFromSource(source string) string {
for k, v := range mc.requiredProviders {
if v.Source == source {
return k
}
}
return ""
}
07070100000051000081A400000000000000000000000168975E3600001C26000000000000000000000000000000000000002800000000tfupdate-0.9.2/tfupdate/context_test.gopackage 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)
}
})
}
}
func TestModuleContextResolveProviderShortNameFromSource(t *testing.T) {
cases := []struct {
desc string
src string
source string
want string
}{
{
desc: "simple",
src: `
terraform {
required_providers {
github = {
source = "integrations/github"
version = "5.38.0"
}
}
}
`,
source: "integrations/github",
want: "github",
},
{
desc: "multiple forks",
src: `
terraform {
required_providers {
petoju = {
source = "petoju/mysql"
version = "3.0.41"
}
winebarrel = {
source = "winebarrel/mysql"
version = "1.10.5"
}
}
}
`,
source: "winebarrel/mysql",
want: "winebarrel",
},
{
desc: "not found",
src: `
terraform {
required_providers {
petoju = {
source = "petoju/mysql"
version = "3.0.41"
}
winebarrel = {
source = "winebarrel/mysql"
version = "1.10.5"
}
}
}
`,
source: "foo/mysql",
want: "",
},
}
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.ResolveProviderShortNameFromSource(tc.source)
if got != tc.want {
t.Errorf("got: %s, want = %s", got, tc.want)
}
})
}
}
func TestModuleContextWithTofuExtension(t *testing.T) {
cases := []struct {
desc string
src string
filename string
want []SelectedProvider
}{
{
desc: "tf extension",
src: `
terraform {
required_version = "1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.4.0"
}
}
}
`,
filename: "main.tf",
want: []SelectedProvider{
{Source: "hashicorp/aws", Version: "5.4.0"},
},
},
{
desc: "tofu extension",
src: `
terraform {
required_version = "1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.4.0"
}
}
}
`,
filename: "main.tofu",
want: []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, tc.filename), []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)
}
})
}
}
07070100000052000081A400000000000000000000000168975E3600000DF4000000000000000000000000000000000000002000000000tfupdate-0.9.2/tfupdate/file.gopackage tfupdate
import (
"bytes"
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/spf13/afero"
)
// UpdateFile updates version constraints in a single file.
// We use an afero filesystem here for testing.
func UpdateFile(ctx context.Context, mc *ModuleContext, filename string) error {
log.Printf("[DEBUG] check file: %s", 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(ctx, mc, r, w, filename)
if err != nil {
return err
}
// Write contents back to source file if changed.
if isUpdated {
log.Printf("[INFO] update file: %s", filename)
updated := w.Bytes()
// We should be able to choose whether to format output or not.
// However, the current implementation of (*hclwrite.Body).SetAttributeValue()
// 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(mc.FS(), filename, result, os.ModePerm); err != nil {
return fmt.Errorf("failed to write file: %s", err)
}
}
return nil
}
// 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 unsupported file type.
func UpdateDir(ctx context.Context, current *ModuleContext, dirname string) error {
log.Printf("[DEBUG] check dir: %s", dirname)
option := current.Option()
dir, err := afero.ReadDir(current.FS(), dirname)
if err != nil {
return fmt.Errorf("failed to open dir: %s", err)
}
for _, entry := range dir {
path := filepath.Join(dirname, entry.Name())
// if a path of entry matches ignorePaths, skip it.
if option.MatchIgnorePaths(path) {
log.Printf("[DEBUG] ignore: %s", path)
continue
}
if entry.IsDir() {
// if an entry is a directory
if !option.recursive {
// skip directory if a recursive flag is false
continue
}
if strings.HasPrefix(entry.Name(), ".") {
// skip hidden directories such as .terraform or .git
continue
}
child, err := NewModuleContext(path, current.GlobalContext())
if err != nil {
return err
}
err = UpdateDir(ctx, child, path)
if err != nil {
return err
}
continue
}
// if an entry is a file
if !(filepath.Ext(entry.Name()) == ".tf" || filepath.Ext(entry.Name()) == ".tofu" || entry.Name() == ".terraform.lock.hcl") {
// skip unsupported file type
continue
}
err := UpdateFile(ctx, current, path)
if err != nil {
return err
}
}
return nil
}
// UpdateFileOrDir updates version constraints in a given file or directory.
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
mc, err := NewModuleContext(path, gc)
if err != nil {
return err
}
return UpdateDir(ctx, mc, path)
}
// if an entry is a file
// 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
}
// When the filename is intentionally specified,
// we should not ignore it by its extension as much as possible.
return UpdateFile(ctx, mc, path)
}
07070100000053000081A400000000000000000000000168975E3600002867000000000000000000000000000000000000002500000000tfupdate-0.9.2/tfupdate/file_test.gopackage tfupdate
import (
"context"
"os"
"path/filepath"
"regexp"
"testing"
"github.com/spf13/afero"
)
func TestUpdateFileExist(t *testing.T) {
cases := []struct {
filename string
src string
o Option
want string
ok bool
}{
{
filename: "valid.tf",
src: `
terraform {
required_version = "0.12.6"
}
`,
o: Option{
updateType: "terraform",
version: "0.12.7",
},
want: `
terraform {
required_version = "0.12.7"
}
`,
ok: true,
},
{
filename: "unformatted_match.tf",
src: `
terraform {
required_version = "0.12.6"
}
`,
o: Option{
updateType: "terraform",
version: "0.12.7",
},
want: `
terraform {
required_version = "0.12.7"
}
`,
ok: true,
},
{
filename: "unformatted_mo_match.tf",
src: `
terraform {
required_version = "0.12.6"
}
`,
o: Option{
updateType: "provider",
name: "aws",
version: "2.23.0",
},
want: `
terraform {
required_version = "0.12.6"
}
`,
ok: true,
},
{
filename: "valid.hcl",
src: `
terraform {
required_version = "0.12.6"
}
`,
o: Option{
updateType: "terraform",
version: "0.12.7",
},
want: `
terraform {
required_version = "0.12.7"
}
`,
ok: true,
},
}
for _, tc := range cases {
fs := afero.NewMemMapFs()
err := afero.WriteFile(fs, tc.filename, []byte(tc.src), 0644)
if err != nil {
t.Fatalf("failed to write file: %s", err)
}
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)
}
if !tc.ok && err == nil {
t.Errorf("UpdateFile() with filename = %s, o = %#v expects to return an error, but no error", tc.filename, tc.o)
}
got, err := afero.ReadFile(fs, tc.filename)
if err != nil {
t.Fatalf("failed to read updated file: %s", err)
}
if string(got) != tc.want {
t.Errorf("UpdateFile() with filename = %s, o = %#v returns %s, but want = %s", tc.filename, tc.o, string(got), tc.want)
}
}
}
func TestUpdateFileNotFound(t *testing.T) {
fs := afero.NewMemMapFs()
filename := "not_found.tf"
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(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)
}
}
func TestUpdateDirExist(t *testing.T) {
cases := []struct {
rootdir string
subdir string
filename1 string
src1 string
filename2 string
src2 string
o Option
checkdir string
want1 string
want2 string
}{
{
rootdir: "a",
subdir: "b",
filename1: "terraform.tf",
src1: `
terraform {
required_version = "0.12.6"
}
`,
filename2: "provider.tf",
src2: `
provider "aws" {
version = "2.11.0"
}
`,
checkdir: "a/b",
o: Option{
updateType: "terraform",
version: "0.12.7",
recursive: false,
},
want1: `
terraform {
required_version = "0.12.7"
}
`,
want2: `
provider "aws" {
version = "2.11.0"
}
`,
},
{
rootdir: "a",
subdir: "b",
filename1: "terraform.tf",
src1: `
terraform {
required_version = "0.12.6"
}
`,
filename2: "provider.tf",
src2: `
provider "aws" {
version = "2.11.0"
}
`,
checkdir: "a",
o: Option{
updateType: "terraform",
version: "0.12.7",
recursive: true,
},
want1: `
terraform {
required_version = "0.12.7"
}
`,
want2: `
provider "aws" {
version = "2.11.0"
}
`,
},
{
rootdir: "a",
subdir: "b",
filename1: "terraform.tf",
src1: `
terraform {
required_version = "0.12.6"
}
`,
filename2: "provider.tf",
src2: `
provider "aws" {
version = "2.11.0"
}
`,
checkdir: "a",
o: Option{
updateType: "terraform",
version: "0.12.7",
recursive: false,
},
want1: `
terraform {
required_version = "0.12.6"
}
`,
want2: `
provider "aws" {
version = "2.11.0"
}
`,
},
{
rootdir: "a",
subdir: ".terraform",
filename1: "terraform.tf",
src1: `
terraform {
required_version = "0.12.6"
}
`,
filename2: "provider.tf",
src2: `
provider "aws" {
version = "2.11.0"
}
`,
checkdir: "a",
o: Option{
updateType: "terraform",
version: "0.12.7",
recursive: true,
},
want1: `
terraform {
required_version = "0.12.6"
}
`,
want2: `
provider "aws" {
version = "2.11.0"
}
`,
},
{
rootdir: "a",
subdir: ".git",
filename1: "terraform.tf",
src1: `
terraform {
required_version = "0.12.6"
}
`,
filename2: "provider.tf",
src2: `
provider "aws" {
version = "2.11.0"
}
`,
checkdir: "a",
o: Option{
updateType: "terraform",
version: "0.12.7",
recursive: true,
},
want1: `
terraform {
required_version = "0.12.6"
}
`,
want2: `
provider "aws" {
version = "2.11.0"
}
`,
},
{
rootdir: "a",
subdir: "b",
filename1: "terraform.hcl",
src1: `
terraform {
required_version = "0.12.6"
}
`,
filename2: "provider.tf",
src2: `
provider "aws" {
version = "2.11.0"
}
`,
checkdir: "a/b",
o: Option{
updateType: "terraform",
version: "0.12.7",
recursive: false,
},
want1: `
terraform {
required_version = "0.12.6"
}
`,
want2: `
provider "aws" {
version = "2.11.0"
}
`,
},
{
rootdir: "a",
subdir: "b",
filename1: "terraform.tf",
src1: `
terraform {
required_version = "0.12.6"
}
`,
filename2: "ignore.tf",
src2: `
terraform {
required_version = "0.12.6"
}
`,
checkdir: "a/b",
o: Option{
updateType: "terraform",
version: "0.12.7",
recursive: false,
ignorePaths: []*regexp.Regexp{regexp.MustCompile(`a/b/ignore.tf`)},
},
want1: `
terraform {
required_version = "0.12.7"
}
`,
want2: `
terraform {
required_version = "0.12.6"
}
`,
},
{
rootdir: "a",
subdir: "b",
filename1: "terraform.tofu",
src1: `
terraform {
required_version = "1.8.0"
}
`,
filename2: "provider.tf",
src2: `
provider "aws" {
version = "2.11.0"
}
`,
checkdir: "a/b",
o: Option{
updateType: "opentofu",
version: "1.9.0",
recursive: false,
},
want1: `
terraform {
required_version = "1.9.0"
}
`,
want2: `
provider "aws" {
version = "2.11.0"
}
`,
},
}
for _, tc := range cases {
fs := afero.NewMemMapFs()
dirname := filepath.Join(tc.rootdir, tc.subdir)
err := fs.MkdirAll(dirname, os.ModePerm)
if err != nil {
t.Fatalf("failed to create dir: %s", err)
}
err = afero.WriteFile(fs, filepath.Join(dirname, tc.filename1), []byte(tc.src1), 0644)
if err != nil {
t.Fatalf("failed to write file: %s", err)
}
err = afero.WriteFile(fs, filepath.Join(dirname, tc.filename2), []byte(tc.src2), 0644)
if err != nil {
t.Fatalf("failed to write file: %s", err)
}
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)
}
got1, err := afero.ReadFile(fs, filepath.Join(dirname, tc.filename1))
if err != nil {
t.Fatalf("failed to read file: %s", err)
}
if string(got1) != tc.want1 {
t.Errorf("UpdateDir() with dirname = %s, o = %#v returns %s, but want = %s", dirname, tc.o, string(got1), tc.want1)
}
got2, err := afero.ReadFile(fs, filepath.Join(dirname, tc.filename2))
if err != nil {
t.Fatalf("failed to read file: %s", err)
}
if string(got2) != tc.want2 {
t.Errorf("UpdateDir() with dirname = %s, o = %#v returns %s, but want = %s", dirname, tc.o, string(got2), tc.want2)
}
}
}
func TestUpdateFileOrDirFile(t *testing.T) {
src := `
terraform {
required_version = "0.12.6"
}
`
o := Option{
updateType: "terraform",
version: "0.12.7",
}
cases := []struct {
desc string
filename string
path string
want string
}{
{
desc: "simple dir with tf file",
filename: "terraform.tf",
path: "a",
want: `
terraform {
required_version = "0.12.7"
}
`,
},
{
desc: "simple tf file",
filename: "terraform.tf",
path: "a/terraform.tf",
want: `
terraform {
required_version = "0.12.7"
}
`,
},
{
desc: "should not update .hcl file if the target path is dir",
filename: "terraform.hcl",
path: "a",
want: `
terraform {
required_version = "0.12.6"
}
`,
},
{
desc: "should update .hcl file if the target path is file",
filename: "terraform.hcl",
path: "a/terraform.hcl",
want: `
terraform {
required_version = "0.12.7"
}
`,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
fs := afero.NewMemMapFs()
dirname := "a"
err := fs.MkdirAll(dirname, os.ModePerm)
if err != nil {
t.Fatalf("failed to create dir: %s", err)
}
err = afero.WriteFile(fs, filepath.Join(dirname, tc.filename), []byte(src), 0644)
if err != nil {
t.Fatalf("failed to write file: %s", err)
}
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)
}
got, err := afero.ReadFile(fs, filepath.Join(dirname, tc.filename))
if err != nil {
t.Fatalf("failed to read file: %s", err)
}
if string(got) != tc.want {
t.Errorf("UpdateFileOrDir() with path = %s, o = %#v returns %s, but want = %s", tc.path, o, string(got), tc.want)
}
})
}
}
07070100000054000081A400000000000000000000000168975E3600001046000000000000000000000000000000000000002400000000tfupdate-0.9.2/tfupdate/hclwrite.gopackage 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
// given name and labels or returns an empty list if there is currently no
// matching block.
func allMatchingBlocks(b *hclwrite.Body, typeName string, labels []string) []*hclwrite.Block {
var matched []*hclwrite.Block
for _, block := range b.Blocks() {
if typeName == block.Type() {
labelNames := block.Labels()
if len(labels) == 0 && len(labelNames) == 0 {
matched = append(matched, block)
continue
}
if reflect.DeepEqual(labels, labelNames) {
matched = append(matched, block)
}
}
}
return matched
}
// allMatchingBlocksByType returns all matching blocks from the body that have the
// given name or returns an empty list if there is currently no matching block.
// This method is useful when you want to ignore label differences.
func allMatchingBlocksByType(b *hclwrite.Body, typeName string) []*hclwrite.Block {
var matched []*hclwrite.Block
for _, block := range b.Blocks() {
if typeName == block.Type() {
matched = append(matched, block)
}
}
return matched
}
// getHCLNativeAttribute gets hclwrite.Attribute as a native hcl.Attribute.
// At the time of writing, there is no way to do with the hclwrite AST,
// so we build low-level byte sequences and parse an attribute as a
// hcl.Attribute on memory.
// If not found, returns nil without an error.
func getHCLNativeAttribute(body *hclwrite.Body, name string) (*hcl.Attribute, error) {
attr := body.GetAttribute(name)
if attr == nil {
return nil, nil
}
// build low-level byte sequences
attrAsBytes := attr.Expr().BuildTokens(nil).Bytes()
src := append([]byte(name+" = "), attrAsBytes...)
// parse an expression as a hcl.File.
// Note that an attribute may contains references, which are defined outside the file.
// So we cannot simply use hclsyntax.ParseExpression or hclsyntax.ParseConfig here.
// We need to use a loe-level parser not to resolve all references.
parser := hclparse.NewParser()
file, diags := parser.ParseHCL(src, "generated_by_getHCLNativeAttribute")
if diags.HasErrors() {
return nil, fmt.Errorf("failed to parse expression: %s", diags)
}
attrs, diags := file.Body.JustAttributes()
if diags.HasErrors() {
return nil, fmt.Errorf("failed to get attributes: %s", diags)
}
hclAttr, ok := attrs[name]
if !ok {
return nil, fmt.Errorf("attribute not found: %s", src)
}
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
}
07070100000055000081A400000000000000000000000168975E36000016CE000000000000000000000000000000000000002900000000tfupdate-0.9.2/tfupdate/hclwrite_test.gopackage tfupdate
import (
"fmt"
"reflect"
"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"
)
func TestAllMatchingBlocks(t *testing.T) {
src := `a = "b"
service {
attr0 = "val0"
}
service {
attr1 = "val1"
}
service "label1" "label2" {
attr2 = "val2"
}
service "label1" "label2" {
attr3 = "val3"
}
`
tests := []struct {
src string
typeName string
labels []string
want string
}{
{
src,
"service",
[]string{},
`service {
attr0 = "val0"
}
service {
attr1 = "val1"
}
`,
},
{
src,
"service",
[]string{"label1", "label2"},
`service "label1" "label2" {
attr2 = "val2"
}
service "label1" "label2" {
attr3 = "val3"
}
`,
},
{
src,
"hoge",
[]string{},
"",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s %s", test.typeName, strings.Join(test.labels, " ")), func(t *testing.T) {
f, diags := hclwrite.ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
blocks := allMatchingBlocks(f.Body(), test.typeName, test.labels)
if len(blocks) == 0 {
if test.want != "" {
t.Fatal("block not found, but want it to exist")
}
} else {
if test.want == "" {
t.Fatal("block found, but expecting not found")
}
got := ""
for _, block := range blocks {
got += string(block.BuildTokens(nil).Bytes())
}
if got != test.want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.want)
}
}
})
}
}
func TestAllMatchingBlocksByType(t *testing.T) {
src := `a = "b"
service {
attr0 = "val0"
}
resource {
attr1 = "val1"
}
service "label1" {
attr2 = "val2"
}
`
tests := []struct {
src string
typeName string
want string
}{
{
src,
"service",
`service {
attr0 = "val0"
}
service "label1" {
attr2 = "val2"
}
`,
},
}
for _, test := range tests {
t.Run(test.typeName, func(t *testing.T) {
f, diags := hclwrite.ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
blocks := allMatchingBlocksByType(f.Body(), test.typeName)
if len(blocks) == 0 {
if test.want != "" {
t.Fatal("block not found, but want it to exist")
}
} else {
if test.want == "" {
t.Fatal("block found, but expecting not found")
}
got := ""
for _, block := range blocks {
got += string(block.BuildTokens(nil).Bytes())
}
if got != test.want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.want)
}
}
})
}
}
func TestGetHCLNativeAttributeValue(t *testing.T) {
cases := []struct {
desc string
src string
name string
wantExprType hcl.Expression
ok bool
}{
{
desc: "string literal",
src: `
foo = "123"
`,
name: "foo",
wantExprType: &hclsyntax.TemplateExpr{},
ok: true,
},
{
desc: "object literal",
src: `
foo = {
bar = "123"
baz = "BAZ"
}
`,
name: "foo",
wantExprType: &hclsyntax.ObjectConsExpr{},
ok: true,
},
{
desc: "object with references",
src: `
foo = {
bar = "123"
baz = "BAZ"
items = [
var.aaa,
var.bbb,
]
}
`,
name: "foo",
wantExprType: &hclsyntax.ObjectConsExpr{},
ok: true,
},
{
desc: "not found",
src: `
foo = "123"
`,
name: "bar",
wantExprType: nil,
ok: true,
},
}
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")
}
got, err := getHCLNativeAttribute(f.Body(), tc.name)
if tc.ok && err != nil {
t.Errorf("unexpected err: %#v", err)
}
if !tc.ok && err == nil {
t.Errorf("expects to return an error, but no error. got = %#v", got)
}
if tc.ok && got != nil {
// An expression is a complicated object and hard to build from literal.
// So we simply compare it by type.
if reflect.TypeOf(got.Expr) != reflect.TypeOf(tc.wantExprType) {
t.Errorf("got = %#v, but want = %#v", got.Expr, tc.wantExprType)
}
}
})
}
}
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)
}
})
}
}
07070100000056000081A400000000000000000000000168975E3600001511000000000000000000000000000000000000002000000000tfupdate-0.9.2/tfupdate/lock.gopackage tfupdate
import (
"context"
"fmt"
"log"
"net/url"
"path/filepath"
"github.com/hashicorp/hcl/v2/hclwrite"
tfaddr "github.com/hashicorp/terraform-registry-address"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/minamijoyo/tfupdate/lock"
"github.com/minamijoyo/tfupdate/tfregistry"
"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
// tfregistryConfig is a configuration for Terraform Registry API.
tfregistryConfig tfregistry.Config
}
// NewLockUpdater is a factory method which returns an LockUpdater instance.
func NewLockUpdater(platforms []string, tfregistryConfig tfregistry.Config) (Updater, error) {
// Create a new index with the provided registry config
index, err := lock.NewIndexFromConfig(tfregistryConfig)
if err != nil {
return nil, err
}
return &LockUpdater{
platforms: platforms,
index: index,
tfregistryConfig: tfregistryConfig,
}, 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 := u.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
}
// fullyQualifiedProviderAddress converts the short form of the provider
// address into the fully qualified form.
// Example: hashicorp/null => registry.terraform.io/hashicorp/null
// If BaseURL is set (e.g., https://registry.opentofu.org/), it will use its hostname
// instead of the default one (e.g., hashicorp/null => registry.opentofu.org/hashicorp/null).
func (u *LockUpdater) fullyQualifiedProviderAddress(address string) (string, error) {
pAddr, err := tfaddr.ParseProviderSource(address)
if err != nil {
return "", fmt.Errorf("failed to parse provider address: %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 address: %s", address)
}
if pAddr.IsLegacy() {
return "", fmt.Errorf("failed to parse legacy provider address: %s", address)
}
// If BaseURL is set, use its hostname
if u.tfregistryConfig.BaseURL != "" {
baseURL, err := url.Parse(u.tfregistryConfig.BaseURL)
if err == nil && baseURL.Hostname() != "" {
// Use the hostname from BaseURL with type casting to svchost.Hostname
pAddr.Hostname = svchost.Hostname(baseURL.Hostname())
}
}
return pAddr.String(), nil
}
07070100000057000081A400000000000000000000000168975E360000E7D2000000000000000000000000000000000000002500000000tfupdate-0.9.2/tfupdate/lock_test.gopackage tfupdate
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/minamijoyo/tfupdate/lock"
"github.com/minamijoyo/tfupdate/tfregistry"
"github.com/spf13/afero"
)
func TestNewLockUpdater(t *testing.T) {
cases := []struct {
platforms []string
tfregistryConfig tfregistry.Config
want *LockUpdater
ok bool
}{
{
platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"},
tfregistryConfig: tfregistry.Config{},
want: &LockUpdater{
platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"},
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
}
for _, tc := range cases {
got, err := NewLockUpdater(tc.platforms, tc.tfregistryConfig)
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 tc.ok {
gotUpdater, ok := got.(*LockUpdater)
if !ok {
t.Errorf("NewLockUpdater() returns %T, want *LockUpdater", got)
continue
}
if diff := cmp.Diff(gotUpdater, tc.want,
cmpopts.IgnoreFields(LockUpdater{}, "index"),
cmp.AllowUnexported(LockUpdater{})); diff != "" {
t.Errorf("NewLockUpdater() with platforms = %s mismatch (-got +want):\n%s", tc.platforms, diff)
}
}
}
}
func TestLockUpdaterFullyQualifiedProviderAddress(t *testing.T) {
cases := []struct {
desc string
address string
tfregistryConfig tfregistry.Config
want string
ok bool
}{
{
desc: "normal provider address",
address: "hashicorp/null",
tfregistryConfig: tfregistry.Config{},
want: "registry.terraform.io/hashicorp/null",
ok: true,
},
{
desc: "already fully qualified provider address",
address: "registry.terraform.io/hashicorp/null",
tfregistryConfig: tfregistry.Config{},
want: "registry.terraform.io/hashicorp/null",
ok: true,
},
{
desc: "with custom BaseURL",
address: "hashicorp/null",
tfregistryConfig: tfregistry.Config{
BaseURL: "https://registry.opentofu.org/",
},
want: "registry.opentofu.org/hashicorp/null",
ok: true,
},
{
desc: "with custom BaseURL and already fully qualified address",
address: "registry.terraform.io/hashicorp/null",
tfregistryConfig: tfregistry.Config{
BaseURL: "https://registry.opentofu.org/",
},
want: "registry.opentofu.org/hashicorp/null",
ok: true,
},
{
desc: "unknown provider address (namespace omitted)",
address: "null",
tfregistryConfig: tfregistry.Config{},
want: "",
ok: false,
},
{
desc: "legacy provider address (namespace as dash)",
address: "-/null",
tfregistryConfig: tfregistry.Config{},
want: "",
ok: false,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
u := &LockUpdater{
tfregistryConfig: tc.tfregistryConfig,
}
got, err := u.fullyQualifiedProviderAddress(tc.address)
if tc.ok && err != nil {
t.Errorf("unexpected error: %s", err)
}
if !tc.ok && err == nil {
t.Errorf("expected error, but got none")
}
if tc.ok && got != tc.want {
t.Errorf("got: %s, want: %s", got, tc.want)
}
})
}
}
func TestUpdateLockWithTerraformRegistry(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)
}
// Create a mock index for testing
mockIndex := lock.NewMockIndex(pvs)
// Create a LockUpdater with empty tfregistryConfig
u, err := NewLockUpdater(platforms, tfregistry.Config{})
// Replace the index with our mock for testing
lu := u.(*LockUpdater)
lu.index = mockIndex
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)
}
})
}
}
func TestUpdateLockWithOpenTofuRegistry(t *testing.T) {
platforms := []string{"darwin_arm64", "darwin_amd64", "linux_amd64"}
pvs := []*lock.ProviderVersion{
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:uQv2oPjJv+ue8bPrVp+So2hHd90UTssnCNajTW554Cw=",
"terraform-provider-null_3.2.1_darwin_amd64.zip": "h1:BdIx478D4wpFgFeaS+5Bdve1HIlt3Yhsr5hAbUs2rRg=",
"terraform-provider-null_3.2.1_darwin_arm64.zip": "h1:+JAon/4CyriC/c7c77NjJalKrKx6gwwQ7L7rVABWMtA=",
},
map[string]string{
"terraform-provider-null_3.2.1_linux_amd64.zip": "zh:40335019c11e5bdb3780301da5197cbc756b4b5ac3d699c52583f1d34e963176",
"terraform-provider-null_3.2.1_linux_arm.zip": "zh:42356e687656fc8ec1f230f786f830f344e64419552ec483e2bc79bd4b7cf1e8",
"terraform-provider-null_3.2.1_darwin_arm64.zip": "zh:5ce03460813954cbebc9f9ad5befbe364d9dc67acb08869f67c1aa634fbf6d6c",
"terraform-provider-null_3.2.1_windows_386.zip": "zh:658ea3e3e7ecc964bdbd08ecde63f3d79f298bab9922b29a6526ba941a4d403a",
"terraform-provider-null_3.2.1_windows_arm64.zip": "zh:68c06703bc57f9c882bfedda6f3047775f0d367093d00efb040800c798b8a613",
"terraform-provider-null_3.2.1_windows_amd64.zip": "zh:80fd03335f793dc54302dd53da98c91fd94f182bcacf13457bed1a99ecffbc1a",
"terraform-provider-null_3.2.1_linux_arm64.zip": "zh:91a76f371815a130735c8fcb6196804d878aebcc67b4c3b73571d2063336ffd8",
"terraform-provider-null_3.2.1_windows_arm.zip": "zh:c146fc0291b7f6284fe4d54ce6aaece6957e9acc93fc572dd505dfd8bcad3e6c",
"terraform-provider-null_3.2.1_linux_386.zip": "zh:c38c9a295cfae9fb6372523c34b9466cd439d5e2c909b56a788960d387c24320",
"terraform-provider-null_3.2.1_darwin_amd64.zip": "zh:e25265d4e87821d18dc9653a0ce01978a1ae5d363bc01dd273454db1aa0309c7",
},
),
}
cases := []struct {
desc string
tfregstryConfig tfregistry.Config
src string
lockfile string
want string
ok bool
}{
{
desc: "simple",
tfregstryConfig: tfregistry.Config{
BaseURL: "https://registry.opentofu.org/",
},
src: `
terraform {
required_version = "1.9.0"
required_providers {
null = {
source = "hashicorp/null"
version = "3.2.1"
}
}
}
`,
lockfile: `
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/null" {
version = "3.1.1"
constraints = "3.1.1"
hashes = [
"h1:Difmh8E1BLxb+7+waC9B76cMdsrcjaHlSY4r0fF2pwo=",
"h1:QitqPlS79NODAC5hplISDtI7b84ETNx3H9nrcJfMgF8=",
"h1:r1SUdAxGh9oAwW1XRSsbfIxGQo29cRvuLxxICN3KjWI=",
"zh:35e3cd3af991a2d6f5c3818d44c4d2c188238782849f79c8ba30caa2f73985a7",
"zh:4b1451dcd6a641a7e618c2bb586bffb8438d9142c357afb645266b94ec1a8450",
"zh:4cc1c7774562bed41d6bac9cfe80d81a2b68c6a13594058ae21d66ce0854db6e",
"zh:63535ceefb44f94482c8b6cf70603d64f966c17772e8e2402dd879b54523396a",
"zh:70de13ff0371960286870f14e7d89d0dd886c2418aaaac8045ed82c8787ceb14",
"zh:79f158d4ac8c25915312a2bc1133c70902974f369a7cedd652439184487ad2c3",
"zh:7f38e45a859fc82e9122a8d01d568c664f723910cc6714421640134a9f974681",
"zh:b5b7fc2833c5c7c2f1bb7cda9dca8c2d34ea2ed8d592cd870d20ae468d1c5650",
"zh:f4183aea2c36dfb015990af272c55606345ddbc586136628b11687f3e99c95a4",
]
}
`,
want: `
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/null" {
version = "3.2.1"
constraints = "3.2.1"
hashes = [
"h1:+JAon/4CyriC/c7c77NjJalKrKx6gwwQ7L7rVABWMtA=",
"h1:BdIx478D4wpFgFeaS+5Bdve1HIlt3Yhsr5hAbUs2rRg=",
"h1:uQv2oPjJv+ue8bPrVp+So2hHd90UTssnCNajTW554Cw=",
"zh:40335019c11e5bdb3780301da5197cbc756b4b5ac3d699c52583f1d34e963176",
"zh:42356e687656fc8ec1f230f786f830f344e64419552ec483e2bc79bd4b7cf1e8",
"zh:5ce03460813954cbebc9f9ad5befbe364d9dc67acb08869f67c1aa634fbf6d6c",
"zh:658ea3e3e7ecc964bdbd08ecde63f3d79f298bab9922b29a6526ba941a4d403a",
"zh:68c06703bc57f9c882bfedda6f3047775f0d367093d00efb040800c798b8a613",
"zh:80fd03335f793dc54302dd53da98c91fd94f182bcacf13457bed1a99ecffbc1a",
"zh:91a76f371815a130735c8fcb6196804d878aebcc67b4c3b73571d2063336ffd8",
"zh:c146fc0291b7f6284fe4d54ce6aaece6957e9acc93fc572dd505dfd8bcad3e6c",
"zh:c38c9a295cfae9fb6372523c34b9466cd439d5e2c909b56a788960d387c24320",
"zh:e25265d4e87821d18dc9653a0ce01978a1ae5d363bc01dd273454db1aa0309c7",
]
}
`,
ok: true,
},
{
desc: "create from empty",
tfregstryConfig: tfregistry.Config{
BaseURL: "https://registry.opentofu.org/",
},
src: `
terraform {
required_version = "1.9.0"
required_providers {
null = {
source = "hashicorp/null"
version = "3.2.1"
}
}
}
`,
lockfile: ``,
want: `
provider "registry.opentofu.org/hashicorp/null" {
version = "3.2.1"
constraints = "3.2.1"
hashes = [
"h1:+JAon/4CyriC/c7c77NjJalKrKx6gwwQ7L7rVABWMtA=",
"h1:BdIx478D4wpFgFeaS+5Bdve1HIlt3Yhsr5hAbUs2rRg=",
"h1:uQv2oPjJv+ue8bPrVp+So2hHd90UTssnCNajTW554Cw=",
"zh:40335019c11e5bdb3780301da5197cbc756b4b5ac3d699c52583f1d34e963176",
"zh:42356e687656fc8ec1f230f786f830f344e64419552ec483e2bc79bd4b7cf1e8",
"zh:5ce03460813954cbebc9f9ad5befbe364d9dc67acb08869f67c1aa634fbf6d6c",
"zh:658ea3e3e7ecc964bdbd08ecde63f3d79f298bab9922b29a6526ba941a4d403a",
"zh:68c06703bc57f9c882bfedda6f3047775f0d367093d00efb040800c798b8a613",
"zh:80fd03335f793dc54302dd53da98c91fd94f182bcacf13457bed1a99ecffbc1a",
"zh:91a76f371815a130735c8fcb6196804d878aebcc67b4c3b73571d2063336ffd8",
"zh:c146fc0291b7f6284fe4d54ce6aaece6957e9acc93fc572dd505dfd8bcad3e6c",
"zh:c38c9a295cfae9fb6372523c34b9466cd439d5e2c909b56a788960d387c24320",
"zh:e25265d4e87821d18dc9653a0ce01978a1ae5d363bc01dd273454db1aa0309c7",
]
}
`,
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)
}
// Create a mock index for testing
mockIndex := lock.NewMockIndex(pvs)
// Create a LockUpdater with the tfregistryConfig
u, err := NewLockUpdater(platforms, tc.tfregstryConfig)
// Replace the index with our mock for testing
lu := u.(*LockUpdater)
lu.index = mockIndex
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)
}
})
}
}
07070100000058000081A400000000000000000000000168975E3600000CA3000000000000000000000000000000000000002200000000tfupdate-0.9.2/tfupdate/module.gopackage tfupdate
import (
"context"
"path/filepath"
"regexp"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
)
// moduleSourceRegexp is a regular expression for module source.
// This is not a complete module source definition, but it is sufficient to
// parse version. Note that a git reference can be branch name, so we need to
// check if it seems to be a version number.
// https://www.terraform.io/docs/modules/sources.html
var moduleSourceRegexp = regexp.MustCompile(`(.+)\?ref=v([0-9]+(\.[0-9]+)*(-.*)*)`)
// ModuleUpdater is a updater implementation which updates the module version constraint.
type ModuleUpdater struct {
name string
nameRegex *regexp.Regexp
version string
}
// NewModuleUpdater is a factory method which returns an ModuleUpdater instance.
func NewModuleUpdater(name string, version string, nameRegex *regexp.Regexp) (Updater, error) {
if len(name) == 0 {
return nil, errors.Errorf("failed to new module updater. name is required")
}
if len(version) == 0 {
return nil, errors.Errorf("failed to new module updater. version is required")
}
return &ModuleUpdater{
name: name,
nameRegex: nameRegex,
version: version,
}, nil
}
// Update updates the module version constraint.
// Note that this method will rewrite the AST passed as an argument.
func (u *ModuleUpdater) Update(_ context.Context, _ *ModuleContext, filename string, f *hclwrite.File) error {
if filepath.Base(filename) == ".terraform.lock.hcl" {
// skip a lock file.
return nil
}
return u.updateModuleBlock(f)
}
func (u *ModuleUpdater) match(name string) bool {
if u.nameRegex == nil {
return u.name == name
}
return u.nameRegex.MatchString(name)
}
func (u *ModuleUpdater) updateModuleBlock(f *hclwrite.File) error {
for _, m := range allMatchingBlocksByType(f.Body(), "module") {
if s := m.Body().GetAttribute("source"); s != nil {
name, version := parseModuleSource(s)
// If this module is a target module
if u.match(name) {
if len(version) == 0 {
// The source attribute doesn't have a version number.
// Set a version to attribute value only if the version key exists.
if m.Body().GetAttribute("version") != nil {
m.Body().SetAttributeValue("version", cty.StringVal(u.version))
}
continue
}
// The source attribute has a version number.
// Update a version reference in the source value.
newSourceValue := name + `?ref=v` + u.version
m.Body().SetAttributeValue("source", cty.StringVal(newSourceValue))
}
}
}
return nil
}
// parseModuleSource parses module source and returns module name and version.
func parseModuleSource(a *hclwrite.Attribute) (string, string) {
tokens := a.Expr().BuildTokens(nil)
if len(tokens) == 3 &&
tokens[0].Type == hclsyntax.TokenOQuote &&
tokens[1].Type == hclsyntax.TokenQuotedLit &&
tokens[2].Type == hclsyntax.TokenCQuote {
source := string(tokens[1].Bytes)
matched := moduleSourceRegexp.FindStringSubmatch(source)
if len(matched) == 0 {
// no version number
return source, ""
}
name := matched[1]
version := matched[2]
return name, version
}
return "", ""
}
07070100000059000081A400000000000000000000000168975E3600001D0B000000000000000000000000000000000000002700000000tfupdate-0.9.2/tfupdate/module_test.gopackage tfupdate
import (
"context"
"reflect"
"regexp"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
)
func TestNewModuleUpdater(t *testing.T) {
cases := []struct {
name string
sourceMatchType string
version string
want Updater
ok bool
}{
{
name: "terraform-aws-modules/vpc/aws",
sourceMatchType: "full",
version: "2.17.0",
want: &ModuleUpdater{
name: "terraform-aws-modules/vpc/aws",
nameRegex: nil,
version: "2.17.0",
},
ok: true,
},
{
name: "",
sourceMatchType: "full",
version: "2.17.0",
want: nil,
ok: false,
},
{
name: "terraform-aws-modules/vpc/aws",
sourceMatchType: "full",
version: "",
want: nil,
ok: false,
},
}
for _, tc := range cases {
got, err := NewModuleUpdater(tc.name, tc.version, nil)
if tc.ok && err != nil {
t.Errorf("NewModuleUpdater() with name = %s, version = %s returns unexpected err: %+v", tc.name, tc.version, err)
}
if !tc.ok && err == nil {
t.Errorf("NewModuleUpdater() with name = %s, version = %s expects to return an error, but no error", tc.name, tc.version)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewModuleUpdater() with name = %s, version = %s returns %#v, but want = %#v", tc.name, tc.version, got, tc.want)
}
}
}
func TestUpdateModule(t *testing.T) {
cases := []struct {
filename string
src string
name string
sourceMatchType string
version string
want string
ok bool
}{
{
filename: "main.tf",
src: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.17.0"
}
`,
name: "terraform-aws-modules/vpc/aws",
version: "2.18.0",
sourceMatchType: "full",
want: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.18.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc1" {
source = "terraform-aws-modules/vpc/aws"
version = "2.17.0"
}
module "vpc2" {
source = "terraform-aws-modules/vpc/aws"
version = "2.17.0"
}
`,
name: "terraform-aws-modules/vpc/aws",
version: "2.18.0",
sourceMatchType: "full",
want: `
module "vpc1" {
source = "terraform-aws-modules/vpc/aws"
version = "2.18.0"
}
module "vpc2" {
source = "terraform-aws-modules/vpc/aws"
version = "2.18.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.17.0"
}
`,
name: "terraform-aws-modules/hoge/aws",
sourceMatchType: "full",
version: "2.18.0",
want: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.17.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
}
`,
name: "terraform-aws-modules/vpc/aws",
sourceMatchType: "full",
version: "2.18.0",
want: `
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0"
}
`,
name: "git::https://example.com/vpc.git",
sourceMatchType: "full",
version: "1.3.0",
want: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.3.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.17.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.17.0"
}
`,
name: "terraform-aws-modules.git/",
version: "2.18.0",
sourceMatchType: "regex",
want: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.18.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.18.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.17.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.17.0"
}
`,
name: "terraform-aws-modules\\.git/.+",
version: "2.18.0",
sourceMatchType: "regex",
want: `
module "vpc1" {
source = "terraform-aws-modules.git/vpc/aws1"
version = "2.18.0"
}
module "vpc2" {
source = "terraform-aws-modules.git/vpc/aws2"
version = "2.18.0"
}
`,
ok: true,
},
}
for _, tc := range cases {
u := &ModuleUpdater{
name: tc.name,
nameRegex: func() *regexp.Regexp {
if tc.sourceMatchType == "regex" {
return regexp.MustCompile(tc.name)
}
return nil
}(),
version: tc.version,
}
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(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)
}
if !tc.ok && err == nil {
t.Errorf("Update() with src = %s, name = %s, version = %s expects to return an error, but no error", tc.src, tc.name, tc.version)
}
got := string(hclwrite.Format(f.BuildTokens(nil).Bytes()))
if got != tc.want {
t.Errorf("Update() with src = %s, name = %s, version = %s returns %s, but want = %s", tc.src, tc.name, tc.version, got, tc.want)
}
}
}
func TestParseModuleSource(t *testing.T) {
cases := []struct {
src string
name string
version string
}{
{
src: `
module "vpc" {
source = "git::https://example.com/vpc.git"
}
`,
name: "git::https://example.com/vpc.git",
version: "",
},
{
src: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1"
}
`,
name: "git::https://example.com/vpc.git",
version: "1",
},
{
src: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2"
}
`,
name: "git::https://example.com/vpc.git",
version: "1.2",
},
{
src: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0"
}
`,
name: "git::https://example.com/vpc.git",
version: "1.2.0",
},
{
src: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0-rc1"
}
`,
name: "git::https://example.com/vpc.git",
version: "1.2.0-rc1",
},
{
src: `
module "vpc" {
source = "git::https://example.com/vpc.git?ref=vhoge"
}
`,
name: "git::https://example.com/vpc.git?ref=vhoge",
version: "",
},
}
for _, tc := range cases {
f, diags := hclwrite.ParseConfig([]byte(tc.src), "", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
t.Fatalf("unexpected diagnostics: %s", diags)
}
m := allMatchingBlocksByType(f.Body(), "module")
if len(m) != 1 {
t.Fatalf("failed to get module block: %s", tc.src)
}
s := m[0].Body().GetAttribute("source")
if s == nil {
t.Fatalf("failed to get module source attribute: %s", tc.src)
}
name, version := parseModuleSource(s)
if !(name == tc.name && version == tc.version) {
t.Errorf("parseModuleSource() with src = %s returns (%s, %s), but want = (%s, %s)", tc.src, name, version, tc.name, tc.version)
}
}
}
0707010000005A000081A400000000000000000000000168975E36000004D0000000000000000000000000000000000000002400000000tfupdate-0.9.2/tfupdate/opentofu.gopackage tfupdate
import (
"context"
"path/filepath"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
)
// OpenTofuUpdater is a updater implementation which updates the OpenTofu version constraint.
type OpenTofuUpdater struct {
version string
}
// NewOpenTofuUpdater is a factory method which returns an OpenTofuUpdater instance.
func NewOpenTofuUpdater(version string) (Updater, error) {
if len(version) == 0 {
return nil, errors.Errorf("failed to new opentofu updater. version is required")
}
return &OpenTofuUpdater{
version: version,
}, nil
}
// Update updates the OpenTofu version constraint.
// Note that this method will rewrite the AST passed as an argument.
func (u *OpenTofuUpdater) Update(_ context.Context, _ *ModuleContext, filename string, f *hclwrite.File) error {
if filepath.Base(filename) == ".terraform.lock.hcl" {
// skip a lock file.
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 {
tf.Body().SetAttributeValue("required_version", cty.StringVal(u.version))
}
}
return nil
}
0707010000005B000081A400000000000000000000000168975E3600000B64000000000000000000000000000000000000002900000000tfupdate-0.9.2/tfupdate/opentofu_test.gopackage tfupdate
import (
"context"
"reflect"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
)
func TestNewOpenTofuUpdater(t *testing.T) {
cases := []struct {
version string
want Updater
ok bool
}{
{
version: "1.9.0",
want: &OpenTofuUpdater{
version: "1.9.0",
},
ok: true,
},
{
version: "",
want: nil,
ok: false,
},
}
for _, tc := range cases {
got, err := NewOpenTofuUpdater(tc.version)
if tc.ok && err != nil {
t.Errorf("NewOpenTofuUpdater() with version = %s returns unexpected err: %+v", tc.version, err)
}
if !tc.ok && err == nil {
t.Errorf("NewOpenTofuUpdater() with version = %s expects to return an error, but no error", tc.version)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewOpenTofuUpdater() with version = %s returns %#v, but want = %#v", tc.version, got, tc.want)
}
}
}
func TestUpdateOpenTofu(t *testing.T) {
cases := []struct {
filename string
src string
version string
want string
ok bool
}{
{
filename: "main.tf",
src: `
terraform {
required_version = "1.8.0"
}
`,
version: "1.9.0",
want: `
terraform {
required_version = "1.9.0"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
null = "2.1.1"
}
}
`,
version: "1.9.0",
want: `
terraform {
required_providers {
null = "2.1.1"
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
provider "aws" {
version = "2.11.0"
region = "ap-northeast-1"
}
`,
version: "1.9.0",
want: `
provider "aws" {
version = "2.11.0"
region = "ap-northeast-1"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `terraform {
backend "s3" {
region = "ap-northeast-1"
bucket = "hoge"
key = "terraform.tfstate"
}
}
terraform {
required_version = "1.8.0"
}
`,
version: "1.9.0",
want: `terraform {
backend "s3" {
region = "ap-northeast-1"
bucket = "hoge"
key = "terraform.tfstate"
}
}
terraform {
required_version = "1.9.0"
}
`,
ok: true,
},
}
for _, tc := range cases {
u := &OpenTofuUpdater{
version: tc.version,
}
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(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)
}
if !tc.ok && err == nil {
t.Errorf("Update() with src = %s, version = %s expects to return an error, but no error", tc.src, tc.version)
}
got := string(hclwrite.Format(f.BuildTokens(nil).Bytes()))
if got != tc.want {
t.Errorf("Update() with src = %s, version = %s returns %s, but want = %s", tc.src, tc.version, got, tc.want)
}
}
}
0707010000005C000081A400000000000000000000000168975E3600000C55000000000000000000000000000000000000002200000000tfupdate-0.9.2/tfupdate/option.gopackage tfupdate
import (
"fmt"
"regexp"
"strings"
"github.com/minamijoyo/tfupdate/tfregistry"
"golang.org/x/exp/slices"
)
// Option is a set of parameters to update.
type Option struct {
// 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 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
// An array of regular expression for paths to ignore.
ignorePaths []*regexp.Regexp
// This field stores the compiled RE2 regex from the provide name parameter.
// In case the sourceMatchType is set to regex this field is used to match the name.
// In case the provided sourceMatchType is full this field is nil.
nameRegex *regexp.Regexp
// tfregistryConfig is a configuration for Terraform Registry API.
tfregistryConfig tfregistry.Config
}
// NewOption returns an option.
func NewOption(updateType string, name string, version string, platforms []string, recursive bool, ignorePaths []string, sourceMatchType string, tfregistryConfig tfregistry.Config) (Option, error) {
regexps := make([]*regexp.Regexp, 0, len(ignorePaths))
for _, ignorePath := range ignorePaths {
if len(ignorePath) == 0 {
continue
}
r, err := regexp.Compile(ignorePath)
if err != nil {
return Option{}, fmt.Errorf("failed to compile regexp for ignorePath: %s", err)
}
regexps = append(regexps, r)
}
nameRegex, err := nameRegex(updateType, name, sourceMatchType)
if err != nil {
return Option{}, err
}
return Option{
updateType: updateType,
name: name,
version: version,
platforms: platforms,
recursive: recursive,
ignorePaths: regexps,
nameRegex: nameRegex,
tfregistryConfig: tfregistryConfig,
}, nil
}
func nameRegex(updateType string, name string, sourceMatchType string) (*regexp.Regexp, error) {
if updateType == "module" {
validSourceMatchTypes := []string{"full", "regex"}
if !slices.Contains[string](validSourceMatchTypes, sourceMatchType) {
return nil, fmt.Errorf("invalid sourceMatchType: %s valid options [%s]", sourceMatchType, strings.Join(validSourceMatchTypes, ","))
} else if sourceMatchType == "regex" {
if len(name) == 0 {
return nil, fmt.Errorf("name is required when sourceMatchType is regex")
}
r, err := regexp.Compile(name)
if err != nil {
return nil, fmt.Errorf("failed to compile regexp for name: %s with error: %s", name, err)
}
return r, nil
}
}
return nil, nil
}
// MatchIgnorePaths returns whether any of the ignore conditions are met.
func (o *Option) MatchIgnorePaths(path string) bool {
for _, r := range o.ignorePaths {
if r.MatchString(path) {
return true
}
}
return false
}
0707010000005D000081A400000000000000000000000168975E3600001F7B000000000000000000000000000000000000002700000000tfupdate-0.9.2/tfupdate/option_test.gopackage tfupdate
import (
"reflect"
"regexp"
"testing"
"github.com/minamijoyo/tfupdate/tfregistry"
)
func TestNewOption(t *testing.T) {
cases := []struct {
updateType string
name string
version string
platforms []string
recursive bool
ignorePaths []string
sourceMatchType string
tfregistryConfig tfregistry.Config
want Option
ok bool
}{
{
updateType: "terraform",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "full",
tfregistryConfig: tfregistry.Config{},
want: Option{
updateType: "terraform",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []*regexp.Regexp{},
nameRegex: nil,
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
{
updateType: "provider",
name: "aws",
version: "2.23.0",
platforms: []string{},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "full",
tfregistryConfig: tfregistry.Config{},
want: Option{
updateType: "provider",
name: "aws",
version: "2.23.0",
platforms: []string{},
recursive: true,
ignorePaths: []*regexp.Regexp{},
nameRegex: nil,
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
{
updateType: "terraform",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{"hoge", "fuga"},
sourceMatchType: "full",
tfregistryConfig: tfregistry.Config{},
want: Option{
updateType: "terraform",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []*regexp.Regexp{regexp.MustCompile("hoge"), regexp.MustCompile("fuga")},
nameRegex: nil,
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
{
updateType: "terraform",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{""},
sourceMatchType: "full",
tfregistryConfig: tfregistry.Config{},
want: Option{
updateType: "terraform",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []*regexp.Regexp{},
nameRegex: nil,
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
{
updateType: "terraform",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{`\`},
sourceMatchType: "",
tfregistryConfig: tfregistry.Config{},
want: Option{},
ok: false,
},
{
updateType: "lock",
version: "",
platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "full",
tfregistryConfig: tfregistry.Config{},
want: Option{
updateType: "lock",
version: "",
platforms: []string{"darwin_arm64", "darwin_amd64", "linux_amd64"},
recursive: true,
ignorePaths: []*regexp.Regexp{},
nameRegex: nil,
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
{
updateType: "module",
name: "terraform-aws-modules/vpc/aws",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "full",
tfregistryConfig: tfregistry.Config{},
want: Option{
updateType: "module",
name: "terraform-aws-modules/vpc/aws",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []*regexp.Regexp{},
nameRegex: nil,
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
{
updateType: "module",
name: `terraform-aws-modules\.git/vpc/aws`,
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "regex",
tfregistryConfig: tfregistry.Config{},
want: Option{
updateType: "module",
name: `terraform-aws-modules\.git/vpc/aws`,
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []*regexp.Regexp{},
nameRegex: regexp.MustCompile(`terraform-aws-modules\.git/vpc/aws`),
tfregistryConfig: tfregistry.Config{},
},
ok: true,
},
{
updateType: "module",
name: "",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "regex",
tfregistryConfig: tfregistry.Config{},
ok: false,
},
{
updateType: "module",
name: `\`,
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "regex",
tfregistryConfig: tfregistry.Config{},
ok: false,
},
{
updateType: "module",
name: "",
version: "0.12.7",
platforms: []string{},
recursive: true,
ignorePaths: []string{},
sourceMatchType: "invalid",
tfregistryConfig: tfregistry.Config{},
ok: false,
},
}
for _, tc := range cases {
got, err := NewOption(tc.updateType, tc.name, tc.version, tc.platforms, tc.recursive, tc.ignorePaths, tc.sourceMatchType, tc.tfregistryConfig)
if tc.ok && err != nil {
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, 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, 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)
}
}
}
func TestOptionMatchIgnorePaths(t *testing.T) {
updateType := "terraform"
version := "0.12.7"
recursive := true
cases := []struct {
o Option
path string
want bool
}{
{
o: Option{
updateType: updateType,
version: version,
recursive: recursive,
ignorePaths: []*regexp.Regexp{regexp.MustCompile(`.*\.tf`)},
},
path: "tmp/main.tf",
want: true,
},
{
o: Option{
updateType: updateType,
version: version,
recursive: recursive,
ignorePaths: []*regexp.Regexp{regexp.MustCompile("tmp/"), regexp.MustCompile("hoge.tf")},
},
path: "tmp/main.tf",
want: true,
},
{
o: Option{
updateType: updateType,
version: version,
recursive: recursive,
ignorePaths: []*regexp.Regexp{regexp.MustCompile("fuga/"), regexp.MustCompile("main.tf")},
},
path: "tmp/main.tf",
want: true,
},
{
o: Option{
updateType: updateType,
version: version,
recursive: recursive,
ignorePaths: []*regexp.Regexp{regexp.MustCompile("fuga/"), regexp.MustCompile("test.tf")},
},
path: "tmp/main.tf",
want: false,
},
}
for _, tc := range cases {
got := tc.o.MatchIgnorePaths(tc.path)
if got != tc.want {
t.Errorf("(*Option).MatchIgnorePaths() with option = %#v, path = %s returns %t, but want = %t", tc.o, tc.path, got, tc.want)
}
}
}
0707010000005E000081A400000000000000000000000168975E360000189D000000000000000000000000000000000000002400000000tfupdate-0.9.2/tfupdate/provider.gopackage tfupdate
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
)
// ProviderUpdater is a updater implementation which updates the provider version constraint.
type ProviderUpdater struct {
name string
version string
}
// NewProviderUpdater is a factory method which returns an ProviderUpdater instance.
func NewProviderUpdater(name string, version string) (Updater, error) {
if len(name) == 0 {
return nil, errors.Errorf("failed to new provider updater. name is required")
}
if len(version) == 0 {
return nil, errors.Errorf("failed to new provider updater. version is required")
}
return &ProviderUpdater{
name: name,
version: version,
}, nil
}
// Update updates the provider version constraint.
// Note that this method will rewrite the AST passed as an argument.
func (u *ProviderUpdater) Update(_ context.Context, mc *ModuleContext, filename string, f *hclwrite.File) error {
if filepath.Base(filename) == ".terraform.lock.hcl" {
// skip a lock file.
return nil
}
if err := u.updateTerraformBlock(mc, f); err != nil {
return err
}
return u.updateProviderBlock(f)
}
func (u *ProviderUpdater) updateTerraformBlock(mc *ModuleContext, f *hclwrite.File) error {
for _, tf := range allMatchingBlocks(f.Body(), "terraform", []string{}) {
p := tf.Body().FirstMatchingBlock("required_providers", []string{})
if p == nil {
continue
}
name := u.name
// If the name contains /, assume that a namespace is intended and check the source.
if strings.Contains(u.name, "/") {
name = mc.ResolveProviderShortNameFromSource(u.name)
if name == "" {
continue
}
}
// The hclwrite.Attribute doesn't have enough AST for object type to check.
// Get the attribute as a native hcl.Attribute as a compromise.
hclAttr, err := getHCLNativeAttribute(p.Body(), name)
if err != nil {
return err
}
if hclAttr != nil {
// There are some variations on the syntax of required_providers.
// So we check a type of the value and switch implementations.
// If the expression can be parsed as a static expression and it's type is a primitive,
// then it's a legacy string syntax.
if expr, err := hclAttr.Expr.Value(nil); err == nil && expr.Type().IsPrimitiveType() {
u.updateTerraformRequiredProvidersBlockAsString(p)
} else {
// Otherwise, it's an object syntax.
if err := u.updateTerraformRequiredProvidersBlockAsObject(p, name, hclAttr); err != nil {
return err
}
}
}
}
return nil
}
func (u *ProviderUpdater) updateTerraformRequiredProvidersBlockAsObject(p *hclwrite.Block, name string, hclAttr *hcl.Attribute) error {
// terraform {
// required_providers {
// aws = {
// source = "hashicorp/aws"
// version = "2.65.0"
//
// configuration_aliases = [
// aws.primary,
// aws.secondary,
// ]
// }
// }
// }
oldVersion, err := detectVersionInObject(hclAttr)
if err != nil {
return err
}
if len(oldVersion) == 0 {
// If the version key is missing, just ignore it.
return nil
}
// Updating the whole object loses original sort order and comments.
// At the time of writing, there is no way to update a value inside an
// object directly while preserving original tokens.
//
// Since we fully understand the valid syntax, we compromise and read the
// tokens in order, updating the bytes directly.
// It's apparently a fragile dirty hack, but I didn't come up with the better
// way to do this.
attr := p.Body().GetAttribute(name)
tokens := attr.Expr().BuildTokens(nil)
i := 0
// find key of version
// Although not explicitly stated in the required_providers documentation,
// a TokenQuotedLit is also valid token. Strict speaking there are more
// variants because the left hand side of object key accepts an expression in
// HCL. For accurate implementation, it should be implemented using the
// original parser.
for !((tokens[i].Type == hclsyntax.TokenIdent || tokens[i].Type == hclsyntax.TokenQuotedLit) &&
string(tokens[i].Bytes) == "version") {
i++
}
// find =
for tokens[i].Type != hclsyntax.TokenEqual {
i++
}
// find value of old version
for !(tokens[i].Type == hclsyntax.TokenQuotedLit && string(tokens[i].Bytes) == oldVersion) {
i++
}
// Since I've checked for the existence of the version key in advance,
// if we reach here, we found the token to be updated.
// So we now update bytes of the token in place.
tokens[i].Bytes = []byte(u.version)
return nil
}
// detectVersionInObject parses an object expression and detects a value for
// the "version" key.
// If the version key is missing, just returns an empty string without an error.
func detectVersionInObject(hclAttr *hcl.Attribute) (string, error) {
// The configuration_aliases syntax isn't directly related version updateing,
// but it contains provider references and causes an parse error without an EvalContext.
// So we treat the expression as a hcl.ExprMap to avoid fully decoding the object.
kvs, diags := hcl.ExprMap(hclAttr.Expr)
if diags.HasErrors() {
return "", fmt.Errorf("failed to parse expr as hcl.ExprMap: %s", diags)
}
oldVersion := ""
for _, kv := range kvs {
key, diags := kv.Key.Value(nil)
if diags.HasErrors() {
return "", fmt.Errorf("failed to get key: %s", diags)
}
if key.AsString() == "version" {
value, diags := kv.Value.Value(nil)
if diags.HasErrors() {
return "", fmt.Errorf("failed to get value: %s", diags)
}
oldVersion = value.AsString()
}
}
return oldVersion, nil
}
func (u *ProviderUpdater) updateTerraformRequiredProvidersBlockAsString(p *hclwrite.Block) {
// terraform {
// required_providers {
// aws = "2.65.0"
// }
// }
p.Body().SetAttributeValue(u.name, cty.StringVal(u.version))
}
func (u *ProviderUpdater) updateProviderBlock(f *hclwrite.File) error {
for _, p := range allMatchingBlocks(f.Body(), "provider", []string{u.name}) {
// set a version to attribute value only if the key exists
if p.Body().GetAttribute("version") != nil {
p.Body().SetAttributeValue("version", cty.StringVal(u.version))
}
}
return nil
}
0707010000005F000081A400000000000000000000000168975E3600002390000000000000000000000000000000000000002900000000tfupdate-0.9.2/tfupdate/provider_test.gopackage tfupdate
import (
"context"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/spf13/afero"
)
func TestNewProviderUpdater(t *testing.T) {
cases := []struct {
name string
version string
want Updater
ok bool
}{
{
name: "aws",
version: "2.23.0",
want: &ProviderUpdater{
name: "aws",
version: "2.23.0",
},
ok: true,
},
{
name: "",
version: "2.23.0",
want: nil,
ok: false,
},
{
name: "aws",
version: "",
want: nil,
ok: false,
},
}
for _, tc := range cases {
got, err := NewProviderUpdater(tc.name, tc.version)
if tc.ok && err != nil {
t.Errorf("NewProviderUpdater() with name = %s, version = %s returns unexpected err: %+v", tc.name, tc.version, err)
}
if !tc.ok && err == nil {
t.Errorf("NewProviderUpdater() with name = %s, version = %s expects to return an error, but no error", tc.name, tc.version)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewProviderUpdater() with name = %s, version = %s returns %#v, but want = %#v", tc.name, tc.version, got, tc.want)
}
}
}
func TestUpdateProvider(t *testing.T) {
cases := []struct {
filename string
src string
name string
version string
want string
ok bool
}{
{
filename: "main.tf",
src: `
terraform {
required_version = "0.12.4"
required_providers {
null = "2.1.1"
}
}
`,
name: "null",
version: "2.1.2",
want: `
terraform {
required_version = "0.12.4"
required_providers {
null = "2.1.2"
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
provider "aws" {
version = "2.11.0"
region = "ap-northeast-1"
}
`,
name: "aws",
version: "2.23.0",
want: `
provider "aws" {
version = "2.23.0"
region = "ap-northeast-1"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_version = "0.12.4"
required_providers {
null = "2.1.1"
}
}
`,
name: "aws",
version: "2.23.0",
want: `
terraform {
required_version = "0.12.4"
required_providers {
null = "2.1.1"
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
provider "aws" {
region = "ap-northeast-1"
}
`,
name: "aws",
version: "2.23.0",
want: `
provider "aws" {
region = "ap-northeast-1"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
null = "2.1.1"
}
}
terraform {
required_providers {
aws = "2.11.0"
}
}
provider "aws" {
alias = "one"
version = "2.11.0"
region = "ap-northeast-1"
}
provider "aws" {
alias = "two"
version = "2.11.0"
region = "us-east-1"
}
`,
name: "aws",
version: "2.23.0",
want: `
terraform {
required_providers {
null = "2.1.1"
}
}
terraform {
required_providers {
aws = "2.23.0"
}
}
provider "aws" {
alias = "one"
version = "2.23.0"
region = "ap-northeast-1"
}
provider "aws" {
alias = "two"
version = "2.23.0"
region = "us-east-1"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "2.65.0"
}
}
}
`,
name: "aws",
version: "2.66.0",
want: `
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "2.66.0"
}
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
`,
name: "aws",
version: "2.66.0",
want: `
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "2.1.2"
}
aws = {
source = "hashicorp/aws"
version = "2.65.0"
}
}
}
`,
name: "aws",
version: "2.66.0",
want: `
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "2.1.2"
}
aws = {
source = "hashicorp/aws"
version = "2.66.0"
}
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
# foo
aws = "2.65.0" # bar
}
}
`,
name: "aws",
version: "2.66.0",
want: `
terraform {
required_providers {
# foo
aws = "2.66.0" # bar
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
# foo
aws = {
# version = "2.65.0" # bar
version = "2.65.0" # baz
source = "hashicorp/aws"
}
}
}
`,
name: "aws",
version: "2.66.0",
want: `
terraform {
required_providers {
# foo
aws = {
# version = "2.65.0" # bar
version = "2.66.0" # baz
source = "hashicorp/aws"
}
}
}
`,
ok: true,
},
{
filename: "main.tf",
// a TokenQuotedLit is also valid token for version
src: `
terraform {
required_providers {
aws = {
"version" = "2.65.0"
"source" = "hashicorp/aws"
}
}
}
`,
name: "aws",
version: "2.66.0",
want: `
terraform {
required_providers {
aws = {
"version" = "2.66.0"
"source" = "hashicorp/aws"
}
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
aws = {
version = "2.65.0"
source = "hashicorp/aws"
configuration_aliases = [
aws.primary,
aws.secondary,
]
}
}
}
`,
name: "aws",
version: "2.66.0",
want: `
terraform {
required_providers {
aws = {
version = "2.66.0"
source = "hashicorp/aws"
configuration_aliases = [
aws.primary,
aws.secondary,
]
}
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
github = {
source = "integrations/github"
version = "5.38.0"
}
}
}
`,
name: "integrations/github",
version: "5.39.0",
want: `
terraform {
required_providers {
github = {
source = "integrations/github"
version = "5.39.0"
}
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
petoju = {
source = "petoju/mysql"
version = "3.0.41"
}
winebarrel = {
source = "winebarrel/mysql"
version = "1.10.5"
}
}
}
`,
name: "winebarrel/mysql",
version: "1.10.6",
want: `
terraform {
required_providers {
petoju = {
source = "petoju/mysql"
version = "3.0.41"
}
winebarrel = {
source = "winebarrel/mysql"
version = "1.10.6"
}
}
}
`,
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,
},
}
for _, tc := range cases {
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)
}
o := Option{
updateType: "provider",
name: tc.name,
version: tc.version,
}
gc, err := NewGlobalContext(fs, 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)
}
u := gc.updater
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(context.Background(), mc, 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)
}
if !tc.ok && err == nil {
t.Errorf("Update() with src = %s, name = %s, version = %s expects to return an error, but no error", tc.src, tc.name, tc.version)
}
got := string(hclwrite.Format(f.BuildTokens(nil).Bytes()))
if got != tc.want {
t.Errorf("Update() with src = %s, name = %s, version = %s returns %s, but want = %s", tc.src, tc.name, tc.version, got, tc.want)
}
}
}
07070100000060000081A400000000000000000000000168975E36000004DA000000000000000000000000000000000000002500000000tfupdate-0.9.2/tfupdate/terraform.gopackage tfupdate
import (
"context"
"path/filepath"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
)
// TerraformUpdater is a updater implementation which updates the terraform version constraint.
type TerraformUpdater struct {
version string
}
// NewTerraformUpdater is a factory method which returns an TerraformUpdater instance.
func NewTerraformUpdater(version string) (Updater, error) {
if len(version) == 0 {
return nil, errors.Errorf("failed to new terraform updater. version is required")
}
return &TerraformUpdater{
version: version,
}, nil
}
// Update updates the terraform version constraint.
// Note that this method will rewrite the AST passed as an argument.
func (u *TerraformUpdater) Update(_ context.Context, _ *ModuleContext, filename string, f *hclwrite.File) error {
if filepath.Base(filename) == ".terraform.lock.hcl" {
// skip a lock file.
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 {
tf.Body().SetAttributeValue("required_version", cty.StringVal(u.version))
}
}
return nil
}
07070100000061000081A400000000000000000000000168975E3600000B76000000000000000000000000000000000000002A00000000tfupdate-0.9.2/tfupdate/terraform_test.gopackage tfupdate
import (
"context"
"reflect"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
)
func TestNewTerraformUpdater(t *testing.T) {
cases := []struct {
version string
want Updater
ok bool
}{
{
version: "0.12.7",
want: &TerraformUpdater{
version: "0.12.7",
},
ok: true,
},
{
version: "",
want: nil,
ok: false,
},
}
for _, tc := range cases {
got, err := NewTerraformUpdater(tc.version)
if tc.ok && err != nil {
t.Errorf("NewTerraformUpdater() with version = %s returns unexpected err: %+v", tc.version, err)
}
if !tc.ok && err == nil {
t.Errorf("NewTerraformUpdater() with version = %s expects to return an error, but no error", tc.version)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewTerraformUpdater() with version = %s returns %#v, but want = %#v", tc.version, got, tc.want)
}
}
}
func TestUpdateTerraform(t *testing.T) {
cases := []struct {
filename string
src string
version string
want string
ok bool
}{
{
filename: "main.tf",
src: `
terraform {
required_version = "0.12.6"
}
`,
version: "0.12.7",
want: `
terraform {
required_version = "0.12.7"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
terraform {
required_providers {
null = "2.1.1"
}
}
`,
version: "0.12.7",
want: `
terraform {
required_providers {
null = "2.1.1"
}
}
`,
ok: true,
},
{
filename: "main.tf",
src: `
provider "aws" {
version = "2.11.0"
region = "ap-northeast-1"
}
`,
version: "0.12.7",
want: `
provider "aws" {
version = "2.11.0"
region = "ap-northeast-1"
}
`,
ok: true,
},
{
filename: "main.tf",
src: `terraform {
backend "s3" {
region = "ap-northeast-1"
bucket = "hoge"
key = "terraform.tfstate"
}
}
terraform {
required_version = "0.12.6"
}
`,
version: "0.12.7",
want: `terraform {
backend "s3" {
region = "ap-northeast-1"
bucket = "hoge"
key = "terraform.tfstate"
}
}
terraform {
required_version = "0.12.7"
}
`,
ok: true,
},
}
for _, tc := range cases {
u := &TerraformUpdater{
version: tc.version,
}
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(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)
}
if !tc.ok && err == nil {
t.Errorf("Update() with src = %s, version = %s expects to return an error, but no error", tc.src, tc.version)
}
got := string(hclwrite.Format(f.BuildTokens(nil).Bytes()))
if got != tc.want {
t.Errorf("Update() with src = %s, version = %s returns %s, but want = %s", tc.src, tc.version, got, tc.want)
}
}
}
07070100000062000081A400000000000000000000000168975E3600000B2E000000000000000000000000000000000000002200000000tfupdate-0.9.2/tfupdate/update.gopackage tfupdate
import (
"bytes"
"context"
"fmt"
"io"
"log"
"runtime/debug"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/pkg/errors"
)
// Updater is an interface which updates a version constraint in HCL.
type Updater interface {
// Update updates a version constraint.
// Note that this method will rewrite the AST passed as an argument.
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) {
switch o.updateType {
case "terraform":
return NewTerraformUpdater(o.version)
case "opentofu":
return NewOpenTofuUpdater(o.version)
case "provider":
return NewProviderUpdater(o.name, o.version)
case "module":
return NewModuleUpdater(o.name, o.version, o.nameRegex)
case "lock":
return NewLockUpdater(o.platforms, o.tfregistryConfig)
default:
return nil, errors.Errorf("failed to new updater. unknown type: %s", o.updateType)
}
}
// UpdateHCL reads HCL from io.Reader, updates version constraints
// and writes updated contents to io.Writer.
// If contents changed successfully, it returns true, or otherwise returns false.
// If an error occurs, Nothing is written to the output stream.
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)
}
f, err := safeParseConfig(input, filename, hcl.Pos{Line: 1, Column: 1})
if err != nil {
return false, err
}
u := mc.Updater()
if err = u.Update(ctx, mc, filename, f); err != nil {
return false, err
}
output := f.BuildTokens(nil).Bytes()
if _, err := w.Write(output); err != nil {
return false, fmt.Errorf("failed to write output: %s", err)
}
isUpdated := !bytes.Equal(input, output)
return isUpdated, nil
}
// safeParseConfig parses config and recovers if panic occurs.
// The current hclwrite implementation is no perfect and will panic if
// unparseable input is given. We just treat it as a parse error so as not to
// surprise users of tfupdate.
func safeParseConfig(src []byte, filename string, start hcl.Pos) (f *hclwrite.File, e error) {
defer func() {
if err := recover(); err != nil {
log.Printf("[DEBUG] failed to parse input: %s\nstacktrace: %s", filename, string(debug.Stack()))
// Set a return value from panic recover
e = fmt.Errorf(`failed to parse input: %s
panic: %s
This may be caused by a bug in the hclwrite parser.
As a workaround, you can ignore this file with --ignore-path option`, filename, err)
}
}()
f, diags := hclwrite.ParseConfig(src, filename, start)
if diags.HasErrors() {
return nil, fmt.Errorf("failed to parse input: %s", diags)
}
return f, nil
}
07070100000063000081A400000000000000000000000168975E360000140E000000000000000000000000000000000000002700000000tfupdate-0.9.2/tfupdate/update_test.gopackage tfupdate
import (
"bytes"
"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) {
cases := []struct {
o Option
want Updater
ok bool
}{
{
o: Option{
updateType: "terraform",
version: "0.12.7",
},
want: &TerraformUpdater{
version: "0.12.7",
},
ok: true,
},
{
o: Option{
updateType: "opentofu",
version: "1.9.0",
},
want: &OpenTofuUpdater{
version: "1.9.0",
},
ok: true,
},
{
o: Option{
updateType: "provider",
name: "aws",
version: "2.23.0",
},
want: &ProviderUpdater{
name: "aws",
version: "2.23.0",
},
ok: true,
},
{
o: Option{
updateType: "module",
name: "terraform-aws-modules/vpc/aws",
version: "2.14.0",
},
want: &ModuleUpdater{
name: "terraform-aws-modules/vpc/aws",
version: "2.14.0",
},
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",
version: "0.0.1",
},
want: nil,
ok: false,
},
}
for _, tc := range cases {
got, err := NewUpdater(tc.o)
if tc.ok && err != nil {
t.Errorf("NewUpdater() with o = %#v returns unexpected err: %+v", tc.o, err)
}
if !tc.ok && err == nil {
t.Errorf("NewUpdater() with o = %#v expects to return an error, but no error", tc.o)
}
opts := []cmp.Option{
cmp.AllowUnexported(TerraformUpdater{}),
cmp.AllowUnexported(OpenTofuUpdater{}),
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)
}
}
}
func TestUpdateHCL(t *testing.T) {
cases := []struct {
src string
o Option
want string
isUpdated bool
ok bool
}{
{
src: `
terraform {
required_version = "0.12.4"
}
`,
o: Option{
updateType: "terraform",
version: "0.12.7",
},
// Note the lack of space here.
// the current implementation of (*hclwrite.Body).SetAttributeValue()
// does not seem to preserve an original SpaceBefore value of attribute.
// This is a bug of upstream.
// We avoid this by formating the output of this function.
want: `
terraform {
required_version ="0.12.7"
}
`,
isUpdated: true,
ok: true,
},
{
src: `
terraform {
required_version = "1.8.0"
}
`,
o: Option{
updateType: "opentofu",
version: "1.9.0",
},
want: `
terraform {
required_version ="1.9.0"
}
`,
isUpdated: true,
ok: true,
},
{
src: `
provider "aws" {
version = "2.11.0"
}
`,
o: Option{
updateType: "provider",
name: "aws",
version: "2.23.0",
},
want: `
provider "aws" {
version ="2.23.0"
}
`,
isUpdated: true,
ok: true,
},
{
src: `
provider "aws" {
version = "2.11.0"
}
`,
o: Option{
updateType: "provider",
name: "hoge",
version: "2.23.0",
},
want: `
provider "aws" {
version = "2.11.0"
}
`,
isUpdated: false,
ok: true,
},
{
src: `
provider "invalid" {
`,
o: Option{
updateType: "provider",
name: "hoge",
version: "2.23.0",
},
want: "",
isUpdated: false,
ok: false,
},
{
// not panic even if a map index is a variable reference
src: `resource "not_panic" "hoge" {
b = a[var.env]
}
`,
o: Option{
updateType: "provider",
name: "hoge",
version: "2.23.0",
},
want: `resource "not_panic" "hoge" {
b = a[var.env]
}
`,
isUpdated: false,
ok: true,
},
}
for _, tc := range cases {
r := bytes.NewBufferString(tc.src)
w := &bytes.Buffer{}
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)
}
if !tc.ok && err == nil {
t.Errorf("UpdateHCL() with src = %s, o = %#v expects to return an error, but no error", tc.src, tc.o)
}
if isUpdated != tc.isUpdated {
t.Errorf("UpdateHCL() with src = %s, o = %#v expects to return isUpdated = %t, but want = %t", tc.src, tc.o, isUpdated, tc.isUpdated)
}
got := w.String()
if got != tc.want {
t.Errorf("UpdateHCL() with src = %s, o = %#v returns %s, but want = %s", tc.src, tc.o, got, tc.want)
}
}
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!845 blocks