├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── goreleaser.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .gitlab-ci.yml ├── .golang-ci.yml ├── .goreleaser.yml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── assets ├── example.png └── logo │ └── logo.png ├── cmd ├── root.go └── root_test.go ├── codecov.yml ├── examples ├── terracove.html ├── terracove.json ├── terracove.xml ├── terraform │ ├── success │ │ ├── applied.txt │ │ ├── example.txt │ │ ├── main.tf │ │ └── terraform.tfstate │ └── tfstate-diff │ │ ├── example.txt │ │ ├── main.tf │ │ └── terraform.tfstate └── terragrunt │ ├── error │ ├── main.tf │ └── terragrunt.hcl │ └── no-resources │ ├── main.tf │ ├── terraform.tfstate │ └── terragrunt.hcl ├── go.mod ├── go.sum ├── install.sh ├── internal └── types │ ├── types.go │ └── types_test.go ├── main.go ├── pkg ├── html │ ├── html.go │ └── template.go ├── report │ ├── report.go │ └── report_test.go └── scan │ ├── scan.go │ └── scan_test.go ├── tests ├── terracove.json └── terracove.xml └── tools └── tools.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: https://www.buymeacoffee.com/elementtech 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "08:00" 8 | labels: 9 | - "dependencies" 10 | commit-message: 11 | prefix: "feat" 12 | include: "scope" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | time: "08:00" 18 | labels: 19 | - "dependencies" 20 | commit-message: 21 | prefix: "chore" 22 | include: "scope" 23 | - package-ecosystem: "docker" 24 | directory: "/" 25 | schedule: 26 | interval: "daily" 27 | time: "08:00" 28 | labels: 29 | - "dependencies" 30 | commit-message: 31 | prefix: "feat" 32 | include: "scope" -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | env: 16 | DOCKER_CLI_EXPERIMENTAL: "enabled" 17 | steps: 18 | - 19 | name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - 24 | name: Set up Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: 1.19 28 | - 29 | name: Set up QEMU 30 | uses: docker/setup-qemu-action@v2 31 | - 32 | name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v2 34 | - 35 | name: ghcr-login 36 | uses: docker/login-action@v2 37 | with: 38 | registry: ghcr.io 39 | username: ${{ github.repository_owner }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | - 42 | name: Run GoReleaser 43 | uses: goreleaser/goreleaser-action@v4 44 | with: 45 | version: ${{ env.GITHUB_REF_NAME }} 46 | args: release --rm-dist 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | paths: 5 | - '**.go' 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.19' 20 | 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | version: v1.50.0 25 | args: -c .golang-ci.yml -v --timeout=5m 26 | env: 27 | GO111MODULES: off -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | go-version: [1.19.x] 10 | os: [ubuntu-latest, macos-latest] 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 2 18 | 19 | - uses: actions/setup-go@v4 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | 23 | - name: go get 24 | run: go get ./... 25 | 26 | - name: go mod tidy 27 | run: go mod tidy 28 | 29 | - name: Setup Terraform 30 | uses: hashicorp/setup-terraform@v2.0.3 31 | 32 | - name: Setup Terragrunt 33 | uses: eLco/setup-terragrunt@v1.0.2 34 | 35 | - name: Run coverage 36 | run: go test -race -coverprofile="coverage.out" -covermode=atomic ./... 37 | 38 | - name: Upload coverage to Codecov 39 | if: matrix.os == 'ubuntu-latest' 40 | run: bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | terracove 2 | vendor/ 3 | coverage.out 4 | /dist 5 | .envrc 6 | manpages/ 7 | dist/ 8 | .DS_Store 9 | .terraform* 10 | .terracove* 11 | junit.xml 12 | terraform.tfstate.backup 13 | /terracove.json 14 | /terracove.yaml 15 | /terracove.xml 16 | /terracove.html -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | image: tetafro/golang-gcc:1.19-alpine 3 | 4 | stages: 5 | - lint 6 | - build 7 | - test 8 | - release 9 | 10 | lint: 11 | stage: lint 12 | before_script: 13 | - wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.41.1 14 | script: 15 | - ./bin/golangci-lint run -c .golang-ci.yml 16 | allow_failure: true 17 | 18 | build: 19 | stage: build 20 | script: 21 | - go build 22 | 23 | test: 24 | stage: test 25 | script: 26 | - go test -v -race "$(go list ./... | grep -v /vendor/)" -v -coverprofile=coverage.out 27 | - go tool cover -func=coverage.out 28 | 29 | release: 30 | stage: release 31 | image: 32 | name: goreleaser/goreleaser:v0.164.0 33 | entrypoint: ["/bin/bash", "-c"] 34 | only: 35 | refs: 36 | - tags 37 | variables: 38 | GITLAB_TOKEN: $GITLAB_TOKEN 39 | 40 | script: 41 | - cd "$CI_PROJECT_DIR" 42 | - goreleaser release --rm-dist 43 | -------------------------------------------------------------------------------- /.golang-ci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | lll: 3 | line-length: 180 4 | linters: 5 | enable-all: true 6 | disable: 7 | - testpackage 8 | - forbidigo 9 | - paralleltest 10 | - exhaustivestruct 11 | - varnamelen 12 | - interfacer 13 | - maligned 14 | - scopelint 15 | - golint 16 | - varcheck 17 | - nosnakecase 18 | - deadcode 19 | - ifshort 20 | - structcheck 21 | - rowserrcheck 22 | - sqlclosecheck 23 | - structcheck 24 | - wastedassign 25 | - exhaustruct 26 | - nolintlint 27 | - wrapcheck -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - CGO_ENABLED=0 4 | 5 | builds: 6 | - binary: terracove 7 | ldflags: -s -w -X main.version={{ .Version }} 8 | goos: 9 | - linux 10 | - darwin 11 | goarch: 12 | - amd64 13 | - arm64 14 | 15 | brews: 16 | - name: terracove 17 | homepage: "https://github.com/elementtech/terracove" 18 | tap: 19 | owner: elementtech 20 | name: homebrew-elementtech 21 | commit_author: 22 | name: elementtech 23 | email: amitai333@gmail.com 24 | 25 | archives: 26 | - builds: 27 | - terracove 28 | format_overrides: 29 | - goos: windows 30 | format: zip 31 | replacements: 32 | darwin: Darwin 33 | linux: Linux 34 | amd64: x86_64 35 | 36 | dockers: 37 | - image_templates: 38 | - "ghcr.io/elementtech/{{.ProjectName}}:{{ .Tag }}-amd64" 39 | dockerfile: Dockerfile 40 | use: buildx 41 | build_flag_templates: 42 | - "--pull" 43 | - "--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/elementtech/terracove/main/README.md" 44 | - '--label=io.artifacthub.package.maintainers=[{"name":"ElementTech","email":"amitai333@gmail.com"}]' 45 | - "--label=io.artifacthub.package.license=MIT" 46 | - "--label=org.opencontainers.image.description=A recursive terraform repository tester powered by Terratest" 47 | - "--label=org.opencontainers.image.created={{.Date}}" 48 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 49 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 50 | - "--label=org.opencontainers.image.version={{.Version}}" 51 | - "--label=org.opencontainers.image.source={{.GitURL}}" 52 | - "--platform=linux/amd64" 53 | - image_templates: 54 | - "ghcr.io/elementtech/{{.ProjectName}}:{{ .Tag }}-arm64" 55 | dockerfile: Dockerfile 56 | use: buildx 57 | build_flag_templates: 58 | - "--pull" 59 | - "--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/elementtech/terracove/main/README.md" 60 | - "--label=io.artifacthub.package.logo-url=https://raw.githubusercontent.com/elementtech/terracove/main/assets/logo/logo.png" 61 | - '--label=io.artifacthub.package.maintainers=[{"name":"ElementTech","email":"amitai333@gmail.com"}]' 62 | - "--label=io.artifacthub.package.license=MIT" 63 | - "--label=org.opencontainers.image.description=A recursive terraform repository tester powered by Terratest" 64 | - "--label=org.opencontainers.image.created={{.Date}}" 65 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 66 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 67 | - "--label=org.opencontainers.image.version={{.Version}}" 68 | - "--label=org.opencontainers.image.source={{.GitURL}}" 69 | - "--platform=linux/arm64" 70 | goarch: arm64 71 | 72 | docker_manifests: 73 | - name_template: "ghcr.io/elementtech/{{.ProjectName}}:{{ .Tag }}" 74 | image_templates: 75 | - "ghcr.io/elementtech/{{.ProjectName}}:{{ .Tag }}-amd64" 76 | - "ghcr.io/elementtech/{{.ProjectName}}:{{ .Tag }}-arm64" 77 | - name_template: "ghcr.io/elementtech/{{.ProjectName}}:latest" 78 | image_templates: 79 | - "ghcr.io/elementtech/{{.ProjectName}}:{{ .Tag }}-amd64" 80 | - "ghcr.io/elementtech/{{.ProjectName}}:{{ .Tag }}-arm64" 81 | 82 | checksum: 83 | name_template: "checksums.txt" 84 | 85 | changelog: 86 | sort: asc 87 | use: github 88 | filters: 89 | exclude: 90 | - "^test:" 91 | - "^chore" 92 | - "merge conflict" 93 | - Merge pull request 94 | - Merge remote-tracking branch 95 | - Merge branch 96 | - go mod tidy 97 | groups: 98 | - title: Dependency updates 99 | regexp: '^.*?(feat|fix)\(deps\)!?:.+$' 100 | order: 300 101 | - title: "New Features" 102 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 103 | order: 100 104 | - title: "Bug fixes" 105 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' 106 | order: 200 107 | - title: "Documentation updates" 108 | regexp: ^.*?doc(\([[:word:]]+\))??!?:.+$ 109 | order: 400 110 | - title: Other work 111 | order: 9999 112 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/tekwizely/pre-commit-golang 3 | rev: v1.0.0-rc.1 4 | hooks: 5 | - id: go-build-mod 6 | - id: go-test-mod 7 | - id: go-vet-mod 8 | - id: go-staticcheck-mod 9 | - id: go-fmt 10 | - id: go-fumpt 11 | - id: go-imports 12 | - id: go-lint 13 | - id: golangci-lint-mod 14 | args: [-c.golang-ci.yml] -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | elementtech. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/devops-infra/docker-terragrunt:aws-azure-gcp-tf-1.4.4-tg-0.45.2 2 | COPY terracove /usr/bin/terracove 3 | WORKDIR /data 4 | ENTRYPOINT ["/usr/bin/terracove"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 elementtech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | projectname?=terracove 2 | 3 | default: help 4 | 5 | .PHONY: help 6 | help: ## list makefile targets 7 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 8 | 9 | .PHONY: build 10 | build: ## build golang binary 11 | @go build -ldflags "-X main.version=$(shell git describe --abbrev=0 --tags)" -o $(projectname) 12 | 13 | .PHONY: install 14 | install: ## install golang binary 15 | @go install -ldflags "-X main.version=$(shell git describe --abbrev=0 --tags)" 16 | 17 | .PHONY: run 18 | run: ## run the app 19 | @go run -ldflags "-X main.version=$(shell git describe --abbrev=0 --tags)" main.go 20 | 21 | .PHONY: bootstrap 22 | bootstrap: ## install build deps 23 | go generate -tags tools tools/tools.go 24 | 25 | PHONY: test 26 | test: clean ## display test coverage 27 | go test --cover -parallel=1 -v -coverprofile=coverage.out ./... 28 | go tool cover -func=coverage.out | sort -rnk3 29 | 30 | PHONY: clean 31 | clean: ## clean up environment 32 | @rm -rf coverage.out dist/ $(projectname) 33 | 34 | PHONY: cover 35 | cover: ## display test coverage 36 | go test -v -race $(shell go list ./... | grep -v /vendor/) -v -coverprofile=coverage.out 37 | go tool cover -func=coverage.out 38 | 39 | PHONY: fmt 40 | fmt: ## format go files 41 | gofumpt -w . 42 | gci write . 43 | 44 | PHONY: lint 45 | lint: ## lint go files 46 | golangci-lint run -c .golang-ci.yml 47 | 48 | .PHONY: pre-commit 49 | pre-commit: ## run pre-commit hooks 50 | pre-commit run --all-files 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | terracove 4 |

5 | 6 |

A recursive terraform repository tester powered by Terratest.

7 | 8 |

9 | drawing 10 | drawing 11 | drawing 12 | drawing 13 | drawing 14 | drawing 15 |

16 | 17 |

18 | Key Features • 19 | Install • 20 | Usage • 21 | Quickstart • 22 | Credits • 23 | Support • 24 | License 25 |

26 | 27 | ![screenshot](./assets/example.png) 28 | 29 |
30 | 31 | 32 | ## Key Features 33 | 34 | * Test in parallel multiple directory paths 35 | * Export Results: 36 | * [junit](./examples/terracove.xml) with `--junit` 37 | * [json](./examples/terracove.json) summary with `--json` 38 | * [html](./examples/terracove.html) report with `--html` 39 | * Generate `%` coverage for each module and root directory 40 | * Ignore Errors and Empty Modules 41 | * Supports [terraform](https://www.terraform.io/) and [terragrunt](https://terragrunt.gruntwork.io/) in the same directory tree 42 | 43 | 44 | ## Install 45 | 46 | The recommended way to install on MacOS is via brew: 47 | 48 | ```sh 49 | brew tap elementtech/elementtech 50 | brew install terracove 51 | ``` 52 | 53 | If you'd like to use Docker, you can use the official image: 54 | ```sh 55 | docker run --rm -v $(pwd):/data ghcr.io/elementtech/terracove /data --json --junit --html 56 | ``` 57 | 58 | Or, you can install directly from release: 59 | ```sh 60 | curl -sS https://raw.githubusercontent.com/elementtech/terracove/main/install.sh | bash 61 | ``` 62 | ## Usage 63 | 64 | ```sh 65 | Usage: 66 | terracove [paths]... [flags] 67 | 68 | Flags: 69 | -e, --exclude strings Exclude directories while parsing tree 70 | -h, --help help for terracove 71 | -w, --html Output HTML Report 72 | --ignore-empty Ignore Modules with 0 Resources 73 | --ignore-errors Ignore Planning Errors 74 | -j, --json Output JSON 75 | -x, --junit Output Junit XML 76 | --minimal Don't Append Raw/JSON Plan to the Exported Output 77 | --o-html string Output HTML Report File (default "terracove.html") 78 | --o-json string Output JSON File (default "terracove.json") 79 | --o-junit string Output Junit XML File (default "terracove.xml") 80 | -t, --validate-tf-by string validate terraform by the existence of [filename] in a directory (default "main.tf") 81 | -g, --validate-tg-by string validate terragrunt by the existence of [filename] in a directory (default "terragrunt.hcl") 82 | -v, --version version for terracove 83 | ``` 84 | 85 | ## Quickstart 86 | > Note that you must have terraform/terragrunt binaries installed on your machine 87 | 88 | > The [examples](./examples) directory contains **4 modules**. 2 of them are [terraform](./examples/terraform) and 2 are [terragrunt](./examples/terragrunt). 89 | > 90 | > **Oh no!** It appears some of them have some problems. Let's see exactly what is going on. 91 | > Clone this repository and give it a try. 92 | 93 | ```sh 94 | git clone https://github.com/elementtech/terracove.git 95 | cd terracove 96 | terracove --minimal --junit --json --html . 97 | # . == examples == examples/terraform examples/terragrunt 98 | ``` 99 | 100 | Open the **terracove.xml**, **terracove.json** or **terracove.html** and observe the results. You should see the following: 101 | 102 | ```json 103 | [ 104 | { 105 | "Timestamp": "1984-01-01T19:32:58+05:00", 106 | "Path": ".", 107 | "Results": [ 108 | { 109 | "Path": "examples/terragrunt/no-resources", 110 | "ResourceCount": 0, 111 | "Coverage": 100, 112 | ... 113 | }, 114 | { 115 | "Path": "examples/terragrunt/error", 116 | "Coverage": 0, 117 | ... 118 | }, 119 | { 120 | "Path": "examples/terraform/tfstate-diff", 121 | "ResourceCount": 2, 122 | "ResourceCountDiff": 1, 123 | "Coverage": 50, 124 | ... 125 | }, 126 | { 127 | "Path": "examples/terraform/success", 128 | "ResourceCount": 2, 129 | "ResourceCountExists": 2, 130 | "Coverage": 100, 131 | ... 132 | } 133 | ], 134 | "Coverage": 62.5 135 | } 136 | ] 137 | ``` 138 | 139 | 140 | ## Credits 141 | 142 | This project uses or is inspired by the following open source projects: 143 | 144 | - [golang-cli-template](https://github.com/FalcoSuessgott/golang-cli-template) 145 | - [terratest](https://terratest.gruntwork.io/) 146 | - [docker-terragrunt](https://github.com/devops-infra/docker-terragrunt) 147 | - [junit2html](https://github.com/kitproj/junit2html) 148 | ## Support 149 | 150 | Buy Me A Coffee 151 | 152 | ## License 153 | 154 | [MIT](LICENSE) 155 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementTech/terracove/e8f51194fee629a6a0483cbcb81fcbe5ace9b3d4/assets/example.png -------------------------------------------------------------------------------- /assets/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementTech/terracove/e8f51194fee629a6a0483cbcb81fcbe5ace9b3d4/assets/logo/logo.png -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/elementtech/terracove/internal/types" 7 | "github.com/elementtech/terracove/pkg/scan" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var OutputOptions types.OutputOptions 12 | var ValidateOptions types.ValidateOptions 13 | var RecursiveOptions types.RecursiveOptions 14 | 15 | func newRootCmd(version string) *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "terracove [paths]...", 18 | Short: "terracove tests a directory tree for terraform/terragrunt diffs", 19 | Long: `terracove provides a recursive way to test the health and validity 20 | of a terraform/terragrunt repository structues. 21 | It plans all modules in parallel and outputs a report 22 | in one of more of the following formats: junit, html or json.`, 23 | Version: version, 24 | Args: cobra.MinimumNArgs(1), 25 | RunE: run, 26 | } 27 | 28 | cmd.Flags().BoolVarP(&OutputOptions.Json, "json", "j", false, "Output JSON") 29 | // cmd.Flags().BoolVarP(&OutputOptions.Yaml, "yaml", "y", false, "Output YAML") 30 | cmd.Flags().BoolVarP(&OutputOptions.Junit, "junit", "x", false, "Output Junit XML") 31 | cmd.Flags().BoolVarP(&OutputOptions.HTML, "html", "w", false, "Output HTML Report") 32 | cmd.Flags().StringVar(&OutputOptions.JsonOutPath, "o-json", "terracove.json", "Output JSON File") 33 | cmd.Flags().BoolVar(&OutputOptions.Minimal, "minimal", false, "Don't Append Raw/JSON Plan to the Exported Output") 34 | cmd.Flags().BoolVar(&OutputOptions.IgnoreError, "ignore-errors", false, "Ignore Planning Errors") 35 | cmd.Flags().BoolVar(&OutputOptions.IgnoreEmpty, "ignore-empty", false, "Ignore Modules with 0 Resources") 36 | // cmd.Flags().StringVar(&OutputOptions.YamlOutPath, "o-yaml", "terracove.yaml", "Output YAML") 37 | cmd.Flags().StringVar(&OutputOptions.JunitOutPath, "o-junit", "terracove.xml", "Output Junit XML File") 38 | cmd.Flags().StringVar(&OutputOptions.HTMLOutPath, "o-html", "terracove.html", "Output HTML Report File") 39 | cmd.Flags().StringSliceVarP(&RecursiveOptions.Exclude, "exclude", "e", []string{}, "Exclude directories while parsing tree") 40 | cmd.Flags().StringVarP(&ValidateOptions.ValidateTerraformBy, "validate-tf-by", "t", "main.tf", "validate terraform by the existence of [filename] in a directory") 41 | cmd.Flags().StringVarP(&ValidateOptions.ValidateTerragruntBy, "validate-tg-by", "g", "terragrunt.hcl", "validate terragrunt by the existence of [filename] in a directory") 42 | return cmd 43 | } 44 | 45 | // Execute invokes the command. 46 | func Execute(version string, testing bool) error { 47 | if err := newRootCmd(version).Execute(); err != nil { 48 | if testing { 49 | return nil 50 | } else { 51 | return fmt.Errorf("error executing root command: %w", err) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func run(cmd *cobra.Command, args []string) error { 59 | scan.TerraformModulesTerratest(args, OutputOptions, ValidateOptions, RecursiveOptions) 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNewRootCmd(t *testing.T) { 11 | version := "1.0.0" 12 | cmd := newRootCmd(version) 13 | 14 | assert.Equal(t, "terracove [paths]...", cmd.Use) 15 | assert.Equal(t, "terracove tests a directory tree for terraform/terragrunt diffs", cmd.Short) 16 | assert.Contains(t, cmd.Long, "terracove provides a recursive way to test the health and validity") 17 | assert.Equal(t, version, cmd.Version) 18 | 19 | assert.False(t, OutputOptions.Json) 20 | assert.False(t, OutputOptions.Junit) 21 | assert.Equal(t, "terracove.json", OutputOptions.JsonOutPath) 22 | assert.Equal(t, "terracove.xml", OutputOptions.JunitOutPath) 23 | assert.Equal(t, "main.tf", ValidateOptions.ValidateTerraformBy) 24 | assert.Equal(t, "terragrunt.hcl", ValidateOptions.ValidateTerragruntBy) 25 | } 26 | 27 | func TestRun(t *testing.T) { 28 | args := []string{"examples"} 29 | var stdout bytes.Buffer 30 | OutputOptions.Json = true 31 | OutputOptions.JsonOutPath = "output.json" 32 | rootCmd := newRootCmd("1.0.0") 33 | rootCmd.SetOut(&stdout) 34 | 35 | err := run(rootCmd, args) 36 | 37 | assert.NoError(t, err) 38 | 39 | } 40 | 41 | func TestExecute(t *testing.T) { 42 | 43 | err := Execute("1.0.0", true) 44 | 45 | assert.NoError(t, err) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 5% 7 | patch: 8 | default: 9 | target: 50% 10 | threshold: 5% 11 | -------------------------------------------------------------------------------- /examples/terracove.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 118 | 119 | 120 |
121 | 122 | 123 |
124 |
62.5% Coverage
125 |

.

126 |
Tuesday, Apr 11, 2023 at 6:43pm
127 |
128 |
129 | 130 |
132 | 133 | examples/terragrunt/no-resources 134 | 135 | Skip 136 | 137 | 138 | 139 | 177 | 178 | 179 |
180 |
181 | 182 | 183 | Initializing the backend... 184 | 185 | Initializing provider plugins... 186 | 187 | Terraform has been successfully initialized! 188 |  189 | You may now begin working with Terraform. Try running "terraform plan" to see 190 | any changes that are required for your infrastructure. All Terraform commands 191 | should now work. 192 | 193 | If you ever set or change modules or backend configuration for Terraform, 194 | rerun this command to reinitialize your working directory. If you forget, other 195 | commands will detect it and remind you to do so if necessary. 196 | 197 | Changes to Outputs: 198 | + output = "one input another input" 199 | 200 | You can apply this plan to save these new output values to the Terraform 201 | state, without changing any real infrastructure. 202 |  203 | ───────────────────────────────────────────────────────────────────────────── 204 | 205 | Saved the plan to: .terracove.plan 206 | 207 | To perform exactly these actions, run the following command to apply: 208 | terraform apply ".terracove.plan" 209 | 210 |
211 |

1.317304889s

212 |
213 |
214 | 215 |
217 | 218 | examples/terragrunt/error 219 | 220 | Error 221 | 222 | 223 | 224 | 262 | 263 | 264 |
265 |
266 | 267 | FatalError{Underlying: error while running command: exit status 1; ╷ 268 | │ Error: Reference to undeclared input variable 269 | │  270 | │  on main.tf line 4, in output "output": 271 | │  4: value = "${var.input} ${var.other_input}" 272 | │  273 | │ An input variable with the name "other_input" has not been declared. This 274 | │ variable can be declared with a variable "other_input" {} block. 275 | ╵ 276 | time=2023-04-11T18:43:57+03:00 level=error msg=Terraform invocation failed in /Users/amitai.getzler/Desktop/Explorium/projects/terracove/examples/terragrunt/error 277 | time=2023-04-11T18:43:57+03:00 level=error msg=1 error occurred: 278 | * exit status 1 279 | 280 | } 281 | 282 |
283 |

1.880412659s

284 |
285 |
286 | 287 |
289 | 290 | examples/terraform/success 291 | 292 | Pass 293 | 294 | 295 | 296 | 297 | 335 | 336 | 337 |
338 |
339 | 340 | local_file.example2: Refreshing state... [id=eba8f1df91816c6e3c9bec7506fe550156e662a3] 341 | local_file.example: Refreshing state... [id=943a702d06f34599aee1f8da8ef9f7296031d699] 342 | 343 | No changes. Your infrastructure matches the configuration. 344 | 345 | Terraform has compared your real infrastructure against your configuration 346 | and found no differences, so no changes are needed. 347 | 348 |
349 |

1.935250789s

350 |
351 |
352 | 353 |
355 | 356 | examples/terraform/tfstate-diff 357 | 358 | Fail 359 | 360 | 361 | 362 | 400 | 401 | 402 |
403 |
404 | 405 | local_file.example: Refreshing state... [id=943a702d06f34599aee1f8da8ef9f7296031d699] 406 | 407 | Terraform used the selected providers to generate the following execution 408 | plan. Resource actions are indicated with the following symbols: 409 | + create 410 | 411 | Terraform will perform the following actions: 412 | 413 |  # local_file.example2 will be created 414 |  + resource "local_file" "example2" { 415 | + content = "I am not applied :(" 416 | + content_base64sha256 = (known after apply) 417 | + content_base64sha512 = (known after apply) 418 | + content_md5 = (known after apply) 419 | + content_sha1 = (known after apply) 420 | + content_sha256 = (known after apply) 421 | + content_sha512 = (known after apply) 422 | + directory_permission = "0777" 423 | + file_permission = "0777" 424 | + filename = "./not_applied.txt" 425 | + id = (known after apply) 426 | } 427 | 428 | Plan: 1 to add, 0 to change, 0 to destroy. 429 |  430 | ───────────────────────────────────────────────────────────────────────────── 431 | 432 | Saved the plan to: .terracove.plan 433 | 434 | To perform exactly these actions, run the following command to apply: 435 | terraform apply ".terracove.plan" 436 | 437 |
438 |

1.942997389s

439 |
440 |
441 | 442 |
443 | 444 | 445 | 446 | 447 | 448 | -------------------------------------------------------------------------------- /examples/terracove.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Timestamp": "2023-04-11T18:43:55+03:00", 4 | "Path": ".", 5 | "Results": [ 6 | { 7 | "Path": "examples/terragrunt/no-resources", 8 | "Error": "", 9 | "ResourceCount": 0, 10 | "ResourceCountExists": 0, 11 | "ResourceCountDiff": 0, 12 | "Coverage": 100, 13 | "Duration": 1317304889, 14 | "PlanJSON": { 15 | "format_version": "1.1", 16 | "terraform_version": "1.4.2", 17 | "variables": { 18 | "input": { 19 | "value": "one input" 20 | }, 21 | "other_input": { 22 | "value": "another input" 23 | } 24 | }, 25 | "planned_values": { 26 | "outputs": { 27 | "output": { 28 | "sensitive": false, 29 | "value": "one input another input", 30 | "type": "string" 31 | } 32 | }, 33 | "root_module": {} 34 | }, 35 | "output_changes": { 36 | "output": { 37 | "actions": [ 38 | "create" 39 | ], 40 | "before": null, 41 | "after": "one input another input", 42 | "after_unknown": false, 43 | "before_sensitive": false, 44 | "after_sensitive": false 45 | } 46 | }, 47 | "prior_state": { 48 | "format_version": "1.0", 49 | "terraform_version": "1.4.2", 50 | "values": { 51 | "outputs": { 52 | "output": { 53 | "sensitive": false, 54 | "value": "one input another input", 55 | "type": "string" 56 | } 57 | }, 58 | "root_module": {} 59 | } 60 | }, 61 | "configuration": { 62 | "root_module": { 63 | "outputs": { 64 | "output": { 65 | "expression": { 66 | "references": [ 67 | "var.input", 68 | "var.other_input" 69 | ] 70 | } 71 | } 72 | }, 73 | "variables": { 74 | "input": {}, 75 | "other_input": {} 76 | } 77 | } 78 | } 79 | }, 80 | "PlanRaw": "\n\u001b[0m\u001b[1mInitializing the backend...\u001b[0m\n\n\u001b[0m\u001b[1mInitializing provider plugins...\u001b[0m\n\n\u001b[0m\u001b[1m\u001b[32mTerraform has been successfully initialized!\u001b[0m\u001b[32m\u001b[0m\n\u001b[0m\u001b[32m\nYou may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.\u001b[0m\n\nChanges to Outputs:\n \u001b[32m+\u001b[0m\u001b[0m output = \"one input another input\"\n\nYou can apply this plan to save these new output values to the Terraform\nstate, without changing any real infrastructure.\n\u001b[90m\n─────────────────────────────────────────────────────────────────────────────\u001b[0m\n\nSaved the plan to: .terracove.plan\n\nTo perform exactly these actions, run the following command to apply:\n terraform apply \".terracove.plan\"", 81 | "ActionNoopCount": 0, 82 | "ActionCreateCount": 0, 83 | "ActionReadCount": 0, 84 | "ActionUpdateCount": 0, 85 | "ActionDeleteCount": 0 86 | }, 87 | { 88 | "Path": "examples/terragrunt/error", 89 | "Error": "FatalError{Underlying: error while running command: exit status 1; \u001b[31m╷\u001b[0m\u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\u001b[1m\u001b[31mError: \u001b[0m\u001b[0m\u001b[1mReference to undeclared input variable\u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\u001b[0m on main.tf line 4, in output \"output\":\n\u001b[31m│\u001b[0m \u001b[0m 4: value = \"${var.input} ${\u001b[4mvar.other_input\u001b[0m}\"\u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\n\u001b[31m│\u001b[0m \u001b[0mAn input variable with the name \"other_input\" has not been declared. This\n\u001b[31m│\u001b[0m \u001b[0mvariable can be declared with a variable \"other_input\" {} block.\n\u001b[31m╵\u001b[0m\u001b[0m\ntime=2023-04-11T18:43:57+03:00 level=error msg=Terraform invocation failed in /Users/amitai.getzler/Desktop/Explorium/projects/terracove/examples/terragrunt/error\ntime=2023-04-11T18:43:57+03:00 level=error msg=1 error occurred:\n\t* exit status 1\n\n}", 90 | "ResourceCount": 0, 91 | "ResourceCountExists": 0, 92 | "ResourceCountDiff": 0, 93 | "Coverage": 0, 94 | "Duration": 1880412659, 95 | "PlanJSON": {}, 96 | "PlanRaw": "FatalError{Underlying: error while running command: exit status 1; \u001b[31m╷\u001b[0m\u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\u001b[1m\u001b[31mError: \u001b[0m\u001b[0m\u001b[1mReference to undeclared input variable\u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\u001b[0m on main.tf line 4, in output \"output\":\n\u001b[31m│\u001b[0m \u001b[0m 4: value = \"${var.input} ${\u001b[4mvar.other_input\u001b[0m}\"\u001b[0m\n\u001b[31m│\u001b[0m \u001b[0m\n\u001b[31m│\u001b[0m \u001b[0mAn input variable with the name \"other_input\" has not been declared. This\n\u001b[31m│\u001b[0m \u001b[0mvariable can be declared with a variable \"other_input\" {} block.\n\u001b[31m╵\u001b[0m\u001b[0m\ntime=2023-04-11T18:43:57+03:00 level=error msg=Terraform invocation failed in /Users/amitai.getzler/Desktop/Explorium/projects/terracove/examples/terragrunt/error\ntime=2023-04-11T18:43:57+03:00 level=error msg=1 error occurred:\n\t* exit status 1\n\n}", 97 | "ActionNoopCount": 0, 98 | "ActionCreateCount": 0, 99 | "ActionReadCount": 0, 100 | "ActionUpdateCount": 0, 101 | "ActionDeleteCount": 0 102 | }, 103 | { 104 | "Path": "examples/terraform/success", 105 | "Error": "", 106 | "ResourceCount": 2, 107 | "ResourceCountExists": 2, 108 | "ResourceCountDiff": 0, 109 | "Coverage": 100, 110 | "Duration": 1935250789, 111 | "PlanJSON": { 112 | "format_version": "1.1", 113 | "terraform_version": "1.4.2", 114 | "planned_values": { 115 | "outputs": { 116 | "hello_world": { 117 | "sensitive": false, 118 | "value": "Hello, World!", 119 | "type": "string" 120 | } 121 | }, 122 | "root_module": { 123 | "resources": [ 124 | { 125 | "address": "local_file.example", 126 | "mode": "managed", 127 | "type": "local_file", 128 | "name": "example", 129 | "provider_name": "registry.terraform.io/hashicorp/local", 130 | "schema_version": 0, 131 | "values": { 132 | "content": "Hello, world!", 133 | "content_base64": null, 134 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 135 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 136 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 137 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 138 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 139 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 140 | "directory_permission": "0777", 141 | "file_permission": "0777", 142 | "filename": "./example.txt", 143 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 144 | "sensitive_content": null, 145 | "source": null 146 | }, 147 | "sensitive_values": {} 148 | }, 149 | { 150 | "address": "local_file.example2", 151 | "mode": "managed", 152 | "type": "local_file", 153 | "name": "example2", 154 | "provider_name": "registry.terraform.io/hashicorp/local", 155 | "schema_version": 0, 156 | "values": { 157 | "content": "I am applied!", 158 | "content_base64": null, 159 | "content_base64sha256": "4WcVQSuvfavY8PssZKiBYJLc73HHAiPebp5lLrHZQMI=", 160 | "content_base64sha512": "5PlxweFFC9cjj36Ae1Rjr/UdsguJh209DxODdJ3tllC722uEW3cdfS61F1iIC7zBNXDqr2PfI6NDNYL7V279LQ==", 161 | "content_md5": "2b45a3421034c84bfbae1435a12c4b0d", 162 | "content_sha1": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 163 | "content_sha256": "e16715412baf7dabd8f0fb2c64a8816092dcef71c70223de6e9e652eb1d940c2", 164 | "content_sha512": "e4f971c1e1450bd7238f7e807b5463aff51db20b89876d3d0f1383749ded9650bbdb6b845b771d7d2eb51758880bbcc13570eaaf63df23a3433582fb576efd2d", 165 | "directory_permission": "0777", 166 | "file_permission": "0777", 167 | "filename": "./applied.txt", 168 | "id": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 169 | "sensitive_content": null, 170 | "source": null 171 | }, 172 | "sensitive_values": {} 173 | } 174 | ] 175 | } 176 | }, 177 | "resource_changes": [ 178 | { 179 | "address": "local_file.example", 180 | "mode": "managed", 181 | "type": "local_file", 182 | "name": "example", 183 | "provider_name": "registry.terraform.io/hashicorp/local", 184 | "change": { 185 | "actions": [ 186 | "no-op" 187 | ], 188 | "before": { 189 | "content": "Hello, world!", 190 | "content_base64": null, 191 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 192 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 193 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 194 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 195 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 196 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 197 | "directory_permission": "0777", 198 | "file_permission": "0777", 199 | "filename": "./example.txt", 200 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 201 | "sensitive_content": null, 202 | "source": null 203 | }, 204 | "after": { 205 | "content": "Hello, world!", 206 | "content_base64": null, 207 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 208 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 209 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 210 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 211 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 212 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 213 | "directory_permission": "0777", 214 | "file_permission": "0777", 215 | "filename": "./example.txt", 216 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 217 | "sensitive_content": null, 218 | "source": null 219 | }, 220 | "after_unknown": {}, 221 | "before_sensitive": { 222 | "sensitive_content": true 223 | }, 224 | "after_sensitive": { 225 | "sensitive_content": true 226 | } 227 | } 228 | }, 229 | { 230 | "address": "local_file.example2", 231 | "mode": "managed", 232 | "type": "local_file", 233 | "name": "example2", 234 | "provider_name": "registry.terraform.io/hashicorp/local", 235 | "change": { 236 | "actions": [ 237 | "no-op" 238 | ], 239 | "before": { 240 | "content": "I am applied!", 241 | "content_base64": null, 242 | "content_base64sha256": "4WcVQSuvfavY8PssZKiBYJLc73HHAiPebp5lLrHZQMI=", 243 | "content_base64sha512": "5PlxweFFC9cjj36Ae1Rjr/UdsguJh209DxODdJ3tllC722uEW3cdfS61F1iIC7zBNXDqr2PfI6NDNYL7V279LQ==", 244 | "content_md5": "2b45a3421034c84bfbae1435a12c4b0d", 245 | "content_sha1": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 246 | "content_sha256": "e16715412baf7dabd8f0fb2c64a8816092dcef71c70223de6e9e652eb1d940c2", 247 | "content_sha512": "e4f971c1e1450bd7238f7e807b5463aff51db20b89876d3d0f1383749ded9650bbdb6b845b771d7d2eb51758880bbcc13570eaaf63df23a3433582fb576efd2d", 248 | "directory_permission": "0777", 249 | "file_permission": "0777", 250 | "filename": "./applied.txt", 251 | "id": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 252 | "sensitive_content": null, 253 | "source": null 254 | }, 255 | "after": { 256 | "content": "I am applied!", 257 | "content_base64": null, 258 | "content_base64sha256": "4WcVQSuvfavY8PssZKiBYJLc73HHAiPebp5lLrHZQMI=", 259 | "content_base64sha512": "5PlxweFFC9cjj36Ae1Rjr/UdsguJh209DxODdJ3tllC722uEW3cdfS61F1iIC7zBNXDqr2PfI6NDNYL7V279LQ==", 260 | "content_md5": "2b45a3421034c84bfbae1435a12c4b0d", 261 | "content_sha1": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 262 | "content_sha256": "e16715412baf7dabd8f0fb2c64a8816092dcef71c70223de6e9e652eb1d940c2", 263 | "content_sha512": "e4f971c1e1450bd7238f7e807b5463aff51db20b89876d3d0f1383749ded9650bbdb6b845b771d7d2eb51758880bbcc13570eaaf63df23a3433582fb576efd2d", 264 | "directory_permission": "0777", 265 | "file_permission": "0777", 266 | "filename": "./applied.txt", 267 | "id": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 268 | "sensitive_content": null, 269 | "source": null 270 | }, 271 | "after_unknown": {}, 272 | "before_sensitive": { 273 | "sensitive_content": true 274 | }, 275 | "after_sensitive": { 276 | "sensitive_content": true 277 | } 278 | } 279 | } 280 | ], 281 | "output_changes": { 282 | "hello_world": { 283 | "actions": [ 284 | "no-op" 285 | ], 286 | "before": "Hello, World!", 287 | "after": "Hello, World!", 288 | "after_unknown": false, 289 | "before_sensitive": false, 290 | "after_sensitive": false 291 | } 292 | }, 293 | "prior_state": { 294 | "format_version": "1.0", 295 | "terraform_version": "1.4.2", 296 | "values": { 297 | "outputs": { 298 | "hello_world": { 299 | "sensitive": false, 300 | "value": "Hello, World!", 301 | "type": "string" 302 | } 303 | }, 304 | "root_module": { 305 | "resources": [ 306 | { 307 | "address": "local_file.example", 308 | "mode": "managed", 309 | "type": "local_file", 310 | "name": "example", 311 | "provider_name": "registry.terraform.io/hashicorp/local", 312 | "schema_version": 0, 313 | "values": { 314 | "content": "Hello, world!", 315 | "content_base64": null, 316 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 317 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 318 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 319 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 320 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 321 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 322 | "directory_permission": "0777", 323 | "file_permission": "0777", 324 | "filename": "./example.txt", 325 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 326 | "sensitive_content": null, 327 | "source": null 328 | }, 329 | "sensitive_values": {} 330 | }, 331 | { 332 | "address": "local_file.example2", 333 | "mode": "managed", 334 | "type": "local_file", 335 | "name": "example2", 336 | "provider_name": "registry.terraform.io/hashicorp/local", 337 | "schema_version": 0, 338 | "values": { 339 | "content": "I am applied!", 340 | "content_base64": null, 341 | "content_base64sha256": "4WcVQSuvfavY8PssZKiBYJLc73HHAiPebp5lLrHZQMI=", 342 | "content_base64sha512": "5PlxweFFC9cjj36Ae1Rjr/UdsguJh209DxODdJ3tllC722uEW3cdfS61F1iIC7zBNXDqr2PfI6NDNYL7V279LQ==", 343 | "content_md5": "2b45a3421034c84bfbae1435a12c4b0d", 344 | "content_sha1": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 345 | "content_sha256": "e16715412baf7dabd8f0fb2c64a8816092dcef71c70223de6e9e652eb1d940c2", 346 | "content_sha512": "e4f971c1e1450bd7238f7e807b5463aff51db20b89876d3d0f1383749ded9650bbdb6b845b771d7d2eb51758880bbcc13570eaaf63df23a3433582fb576efd2d", 347 | "directory_permission": "0777", 348 | "file_permission": "0777", 349 | "filename": "./applied.txt", 350 | "id": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 351 | "sensitive_content": null, 352 | "source": null 353 | }, 354 | "sensitive_values": {} 355 | } 356 | ] 357 | } 358 | } 359 | }, 360 | "configuration": { 361 | "provider_config": { 362 | "local": { 363 | "name": "local", 364 | "full_name": "registry.terraform.io/hashicorp/local" 365 | } 366 | }, 367 | "root_module": { 368 | "outputs": { 369 | "hello_world": { 370 | "expression": { 371 | "constant_value": "Hello, World!" 372 | } 373 | } 374 | }, 375 | "resources": [ 376 | { 377 | "address": "local_file.example", 378 | "mode": "managed", 379 | "type": "local_file", 380 | "name": "example", 381 | "provider_config_key": "local", 382 | "expressions": { 383 | "content": { 384 | "constant_value": "Hello, world!" 385 | }, 386 | "filename": { 387 | "references": [ 388 | "path.module" 389 | ] 390 | } 391 | }, 392 | "schema_version": 0 393 | }, 394 | { 395 | "address": "local_file.example2", 396 | "mode": "managed", 397 | "type": "local_file", 398 | "name": "example2", 399 | "provider_config_key": "local", 400 | "expressions": { 401 | "content": { 402 | "constant_value": "I am applied!" 403 | }, 404 | "filename": { 405 | "references": [ 406 | "path.module" 407 | ] 408 | } 409 | }, 410 | "schema_version": 0 411 | } 412 | ] 413 | } 414 | } 415 | }, 416 | "PlanRaw": "\u001b[0m\u001b[1mlocal_file.example2: Refreshing state... [id=eba8f1df91816c6e3c9bec7506fe550156e662a3]\u001b[0m\n\u001b[0m\u001b[1mlocal_file.example: Refreshing state... [id=943a702d06f34599aee1f8da8ef9f7296031d699]\u001b[0m\n\n\u001b[0m\u001b[1m\u001b[32mNo changes.\u001b[0m\u001b[1m Your infrastructure matches the configuration.\u001b[0m\n\n\u001b[0mTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.", 417 | "ActionNoopCount": 2, 418 | "ActionCreateCount": 0, 419 | "ActionReadCount": 0, 420 | "ActionUpdateCount": 0, 421 | "ActionDeleteCount": 0 422 | }, 423 | { 424 | "Path": "examples/terraform/tfstate-diff", 425 | "Error": "", 426 | "ResourceCount": 2, 427 | "ResourceCountExists": 1, 428 | "ResourceCountDiff": 1, 429 | "Coverage": 50, 430 | "Duration": 1942997389, 431 | "PlanJSON": { 432 | "format_version": "1.1", 433 | "terraform_version": "1.4.2", 434 | "planned_values": { 435 | "outputs": { 436 | "hello_world": { 437 | "sensitive": false, 438 | "value": "Hello, World!", 439 | "type": "string" 440 | } 441 | }, 442 | "root_module": { 443 | "resources": [ 444 | { 445 | "address": "local_file.example", 446 | "mode": "managed", 447 | "type": "local_file", 448 | "name": "example", 449 | "provider_name": "registry.terraform.io/hashicorp/local", 450 | "schema_version": 0, 451 | "values": { 452 | "content": "Hello, world!", 453 | "content_base64": null, 454 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 455 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 456 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 457 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 458 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 459 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 460 | "directory_permission": "0777", 461 | "file_permission": "0777", 462 | "filename": "./example.txt", 463 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 464 | "sensitive_content": null, 465 | "source": null 466 | }, 467 | "sensitive_values": {} 468 | }, 469 | { 470 | "address": "local_file.example2", 471 | "mode": "managed", 472 | "type": "local_file", 473 | "name": "example2", 474 | "provider_name": "registry.terraform.io/hashicorp/local", 475 | "schema_version": 0, 476 | "values": { 477 | "content": "I am not applied :(", 478 | "content_base64": null, 479 | "directory_permission": "0777", 480 | "file_permission": "0777", 481 | "filename": "./not_applied.txt", 482 | "sensitive_content": null, 483 | "source": null 484 | }, 485 | "sensitive_values": {} 486 | } 487 | ] 488 | } 489 | }, 490 | "resource_changes": [ 491 | { 492 | "address": "local_file.example", 493 | "mode": "managed", 494 | "type": "local_file", 495 | "name": "example", 496 | "provider_name": "registry.terraform.io/hashicorp/local", 497 | "change": { 498 | "actions": [ 499 | "no-op" 500 | ], 501 | "before": { 502 | "content": "Hello, world!", 503 | "content_base64": null, 504 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 505 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 506 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 507 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 508 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 509 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 510 | "directory_permission": "0777", 511 | "file_permission": "0777", 512 | "filename": "./example.txt", 513 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 514 | "sensitive_content": null, 515 | "source": null 516 | }, 517 | "after": { 518 | "content": "Hello, world!", 519 | "content_base64": null, 520 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 521 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 522 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 523 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 524 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 525 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 526 | "directory_permission": "0777", 527 | "file_permission": "0777", 528 | "filename": "./example.txt", 529 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 530 | "sensitive_content": null, 531 | "source": null 532 | }, 533 | "after_unknown": {}, 534 | "before_sensitive": { 535 | "sensitive_content": true 536 | }, 537 | "after_sensitive": { 538 | "sensitive_content": true 539 | } 540 | } 541 | }, 542 | { 543 | "address": "local_file.example2", 544 | "mode": "managed", 545 | "type": "local_file", 546 | "name": "example2", 547 | "provider_name": "registry.terraform.io/hashicorp/local", 548 | "change": { 549 | "actions": [ 550 | "create" 551 | ], 552 | "before": null, 553 | "after": { 554 | "content": "I am not applied :(", 555 | "content_base64": null, 556 | "directory_permission": "0777", 557 | "file_permission": "0777", 558 | "filename": "./not_applied.txt", 559 | "sensitive_content": null, 560 | "source": null 561 | }, 562 | "after_unknown": { 563 | "content_base64sha256": true, 564 | "content_base64sha512": true, 565 | "content_md5": true, 566 | "content_sha1": true, 567 | "content_sha256": true, 568 | "content_sha512": true, 569 | "id": true 570 | }, 571 | "before_sensitive": false, 572 | "after_sensitive": { 573 | "sensitive_content": true 574 | } 575 | } 576 | } 577 | ], 578 | "output_changes": { 579 | "hello_world": { 580 | "actions": [ 581 | "no-op" 582 | ], 583 | "before": "Hello, World!", 584 | "after": "Hello, World!", 585 | "after_unknown": false, 586 | "before_sensitive": false, 587 | "after_sensitive": false 588 | } 589 | }, 590 | "prior_state": { 591 | "format_version": "1.0", 592 | "terraform_version": "1.4.2", 593 | "values": { 594 | "outputs": { 595 | "hello_world": { 596 | "sensitive": false, 597 | "value": "Hello, World!", 598 | "type": "string" 599 | } 600 | }, 601 | "root_module": { 602 | "resources": [ 603 | { 604 | "address": "local_file.example", 605 | "mode": "managed", 606 | "type": "local_file", 607 | "name": "example", 608 | "provider_name": "registry.terraform.io/hashicorp/local", 609 | "schema_version": 0, 610 | "values": { 611 | "content": "Hello, world!", 612 | "content_base64": null, 613 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 614 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 615 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 616 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 617 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 618 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 619 | "directory_permission": "0777", 620 | "file_permission": "0777", 621 | "filename": "./example.txt", 622 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 623 | "sensitive_content": null, 624 | "source": null 625 | }, 626 | "sensitive_values": {} 627 | } 628 | ] 629 | } 630 | } 631 | }, 632 | "configuration": { 633 | "provider_config": { 634 | "local": { 635 | "name": "local", 636 | "full_name": "registry.terraform.io/hashicorp/local" 637 | } 638 | }, 639 | "root_module": { 640 | "outputs": { 641 | "hello_world": { 642 | "expression": { 643 | "constant_value": "Hello, World!" 644 | } 645 | } 646 | }, 647 | "resources": [ 648 | { 649 | "address": "local_file.example", 650 | "mode": "managed", 651 | "type": "local_file", 652 | "name": "example", 653 | "provider_config_key": "local", 654 | "expressions": { 655 | "content": { 656 | "constant_value": "Hello, world!" 657 | }, 658 | "filename": { 659 | "references": [ 660 | "path.module" 661 | ] 662 | } 663 | }, 664 | "schema_version": 0 665 | }, 666 | { 667 | "address": "local_file.example2", 668 | "mode": "managed", 669 | "type": "local_file", 670 | "name": "example2", 671 | "provider_config_key": "local", 672 | "expressions": { 673 | "content": { 674 | "constant_value": "I am not applied :(" 675 | }, 676 | "filename": { 677 | "references": [ 678 | "path.module" 679 | ] 680 | } 681 | }, 682 | "schema_version": 0 683 | } 684 | ] 685 | } 686 | } 687 | }, 688 | "PlanRaw": "\u001b[0m\u001b[1mlocal_file.example: Refreshing state... [id=943a702d06f34599aee1f8da8ef9f7296031d699]\u001b[0m\n\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n \u001b[32m+\u001b[0m create\u001b[0m\n\nTerraform will perform the following actions:\n\n\u001b[1m # local_file.example2\u001b[0m will be created\n\u001b[0m \u001b[32m+\u001b[0m\u001b[0m resource \"local_file\" \"example2\" {\n \u001b[32m+\u001b[0m\u001b[0m content = \"I am not applied :(\"\n \u001b[32m+\u001b[0m\u001b[0m content_base64sha256 = (known after apply)\n \u001b[32m+\u001b[0m\u001b[0m content_base64sha512 = (known after apply)\n \u001b[32m+\u001b[0m\u001b[0m content_md5 = (known after apply)\n \u001b[32m+\u001b[0m\u001b[0m content_sha1 = (known after apply)\n \u001b[32m+\u001b[0m\u001b[0m content_sha256 = (known after apply)\n \u001b[32m+\u001b[0m\u001b[0m content_sha512 = (known after apply)\n \u001b[32m+\u001b[0m\u001b[0m directory_permission = \"0777\"\n \u001b[32m+\u001b[0m\u001b[0m file_permission = \"0777\"\n \u001b[32m+\u001b[0m\u001b[0m filename = \"./not_applied.txt\"\n \u001b[32m+\u001b[0m\u001b[0m id = (known after apply)\n }\n\n\u001b[1mPlan:\u001b[0m 1 to add, 0 to change, 0 to destroy.\n\u001b[0m\u001b[90m\n─────────────────────────────────────────────────────────────────────────────\u001b[0m\n\nSaved the plan to: .terracove.plan\n\nTo perform exactly these actions, run the following command to apply:\n terraform apply \".terracove.plan\"", 689 | "ActionNoopCount": 1, 690 | "ActionCreateCount": 1, 691 | "ActionReadCount": 0, 692 | "ActionUpdateCount": 0, 693 | "ActionDeleteCount": 0 694 | } 695 | ], 696 | "Coverage": 62.5 697 | } 698 | ] -------------------------------------------------------------------------------- /examples/terracove.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | �[0m�[1mInitializing the backend...�[0m �[0m�[1mInitializing provider plugins...�[0m �[0m�[1m�[32mTerraform has been successfully initialized!�[0m�[32m�[0m �[0m�[32m You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.�[0m Changes to Outputs: �[32m+�[0m�[0m output = "one input another input" You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure. �[90m ─────────────────────────────────────────────────────────────────────────────�[0m Saved the plan to: .terracove.plan To perform exactly these actions, run the following command to apply: terraform apply ".terracove.plan" 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | FatalError{Underlying: error while running command: exit status 1; �[31m╷�[0m�[0m �[31m│�[0m �[0m�[1m�[31mError: �[0m�[0m�[1mReference to undeclared input variable�[0m �[31m│�[0m �[0m �[31m│�[0m �[0m�[0m on main.tf line 4, in output "output": �[31m│�[0m �[0m 4: value = "${var.input} ${�[4mvar.other_input�[0m}"�[0m �[31m│�[0m �[0m �[31m│�[0m �[0mAn input variable with the name "other_input" has not been declared. This �[31m│�[0m �[0mvariable can be declared with a variable "other_input" {} block. �[31m╵�[0m�[0m time=2023-04-11T18:43:57+03:00 level=error msg=Terraform invocation failed in /Users/amitai.getzler/Desktop/Explorium/projects/terracove/examples/terragrunt/error time=2023-04-11T18:43:57+03:00 level=error msg=1 error occurred: * exit status 1 } 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | �[0m�[1mlocal_file.example: Refreshing state... [id=943a702d06f34599aee1f8da8ef9f7296031d699]�[0m Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: �[32m+�[0m create�[0m Terraform will perform the following actions: �[1m # local_file.example2�[0m will be created �[0m �[32m+�[0m�[0m resource "local_file" "example2" { �[32m+�[0m�[0m content = "I am not applied :(" �[32m+�[0m�[0m content_base64sha256 = (known after apply) �[32m+�[0m�[0m content_base64sha512 = (known after apply) �[32m+�[0m�[0m content_md5 = (known after apply) �[32m+�[0m�[0m content_sha1 = (known after apply) �[32m+�[0m�[0m content_sha256 = (known after apply) �[32m+�[0m�[0m content_sha512 = (known after apply) �[32m+�[0m�[0m directory_permission = "0777" �[32m+�[0m�[0m file_permission = "0777" �[32m+�[0m�[0m filename = "./not_applied.txt" �[32m+�[0m�[0m id = (known after apply) } �[1mPlan:�[0m 1 to add, 0 to change, 0 to destroy. �[0m�[90m ─────────────────────────────────────────────────────────────────────────────�[0m Saved the plan to: .terracove.plan To perform exactly these actions, run the following command to apply: terraform apply ".terracove.plan" 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/terraform/success/applied.txt: -------------------------------------------------------------------------------- 1 | I am applied! -------------------------------------------------------------------------------- /examples/terraform/success/example.txt: -------------------------------------------------------------------------------- 1 | Hello, world! -------------------------------------------------------------------------------- /examples/terraform/success/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting 3 | # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it 4 | # forwards compatible with 0.13.x code. 5 | required_version = ">= 0.12.26" 6 | } 7 | 8 | output "hello_world" { 9 | value = "Hello, World!" 10 | } 11 | 12 | resource "local_file" "example" { 13 | content = "Hello, world!" 14 | filename = "${path.module}/example.txt" 15 | } 16 | 17 | resource "local_file" "example2" { 18 | content = "I am applied!" 19 | filename = "${path.module}/applied.txt" 20 | } 21 | -------------------------------------------------------------------------------- /examples/terraform/success/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.4.2", 4 | "serial": 6, 5 | "lineage": "6e0e7ecf-59fb-a972-977b-f787d70d1a76", 6 | "outputs": { 7 | "hello_world": { 8 | "value": "Hello, World!", 9 | "type": "string" 10 | } 11 | }, 12 | "resources": [ 13 | { 14 | "mode": "managed", 15 | "type": "local_file", 16 | "name": "example", 17 | "provider": "provider[\"registry.terraform.io/hashicorp/local\"]", 18 | "instances": [ 19 | { 20 | "schema_version": 0, 21 | "attributes": { 22 | "content": "Hello, world!", 23 | "content_base64": null, 24 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 25 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 26 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 27 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 28 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 29 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 30 | "directory_permission": "0777", 31 | "file_permission": "0777", 32 | "filename": "./example.txt", 33 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 34 | "sensitive_content": null, 35 | "source": null 36 | }, 37 | "sensitive_attributes": [] 38 | } 39 | ] 40 | }, 41 | { 42 | "mode": "managed", 43 | "type": "local_file", 44 | "name": "example2", 45 | "provider": "provider[\"registry.terraform.io/hashicorp/local\"]", 46 | "instances": [ 47 | { 48 | "schema_version": 0, 49 | "attributes": { 50 | "content": "I am applied!", 51 | "content_base64": null, 52 | "content_base64sha256": "4WcVQSuvfavY8PssZKiBYJLc73HHAiPebp5lLrHZQMI=", 53 | "content_base64sha512": "5PlxweFFC9cjj36Ae1Rjr/UdsguJh209DxODdJ3tllC722uEW3cdfS61F1iIC7zBNXDqr2PfI6NDNYL7V279LQ==", 54 | "content_md5": "2b45a3421034c84bfbae1435a12c4b0d", 55 | "content_sha1": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 56 | "content_sha256": "e16715412baf7dabd8f0fb2c64a8816092dcef71c70223de6e9e652eb1d940c2", 57 | "content_sha512": "e4f971c1e1450bd7238f7e807b5463aff51db20b89876d3d0f1383749ded9650bbdb6b845b771d7d2eb51758880bbcc13570eaaf63df23a3433582fb576efd2d", 58 | "directory_permission": "0777", 59 | "file_permission": "0777", 60 | "filename": "./applied.txt", 61 | "id": "eba8f1df91816c6e3c9bec7506fe550156e662a3", 62 | "sensitive_content": null, 63 | "source": null 64 | }, 65 | "sensitive_attributes": [] 66 | } 67 | ] 68 | } 69 | ], 70 | "check_results": null 71 | } 72 | -------------------------------------------------------------------------------- /examples/terraform/tfstate-diff/example.txt: -------------------------------------------------------------------------------- 1 | Hello, world! -------------------------------------------------------------------------------- /examples/terraform/tfstate-diff/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting 3 | # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it 4 | # forwards compatible with 0.13.x code. 5 | required_version = ">= 0.12.26" 6 | } 7 | 8 | output "hello_world" { 9 | value = "Hello, World!" 10 | } 11 | 12 | resource "local_file" "example" { 13 | content = "Hello, world!" 14 | filename = "${path.module}/example.txt" 15 | } 16 | 17 | resource "local_file" "example2" { 18 | content = "I am not applied :(" 19 | filename = "${path.module}/not_applied.txt" 20 | } 21 | -------------------------------------------------------------------------------- /examples/terraform/tfstate-diff/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.4.2", 4 | "serial": 3, 5 | "lineage": "14da17f5-056b-603b-5d03-fd52a88a01f8", 6 | "outputs": { 7 | "hello_world": { 8 | "value": "Hello, World!", 9 | "type": "string" 10 | } 11 | }, 12 | "resources": [ 13 | { 14 | "mode": "managed", 15 | "type": "local_file", 16 | "name": "example", 17 | "provider": "provider[\"registry.terraform.io/hashicorp/local\"]", 18 | "instances": [ 19 | { 20 | "schema_version": 0, 21 | "attributes": { 22 | "content": "Hello, world!", 23 | "content_base64": null, 24 | "content_base64sha256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", 25 | "content_base64sha512": "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==", 26 | "content_md5": "6cd3556deb0da54bca060b4c39479839", 27 | "content_sha1": "943a702d06f34599aee1f8da8ef9f7296031d699", 28 | "content_sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", 29 | "content_sha512": "c1527cd893c124773d811911970c8fe6e857d6df5dc9226bd8a160614c0cd963a4ddea2b94bb7d36021ef9d865d5cea294a82dd49a0bb269f51f6e7a57f79421", 30 | "directory_permission": "0777", 31 | "file_permission": "0777", 32 | "filename": "./example.txt", 33 | "id": "943a702d06f34599aee1f8da8ef9f7296031d699", 34 | "sensitive_content": null, 35 | "source": null 36 | }, 37 | "sensitive_attributes": [] 38 | } 39 | ] 40 | } 41 | ], 42 | "check_results": null 43 | } 44 | -------------------------------------------------------------------------------- /examples/terragrunt/error/main.tf: -------------------------------------------------------------------------------- 1 | variable "input" {} 2 | 3 | output "output" { 4 | value = "${var.input} ${var.other_input}" 5 | } 6 | 7 | resource "local_file" "example" { 8 | content = "Hello, world!" 9 | filename = "${path.module}/example.txt" 10 | } -------------------------------------------------------------------------------- /examples/terragrunt/error/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | inputs = { 2 | input = "one input" 3 | other_input = "another input" 4 | extraneous_input = "an unused input" 5 | } -------------------------------------------------------------------------------- /examples/terragrunt/no-resources/main.tf: -------------------------------------------------------------------------------- 1 | variable "input" {} 2 | variable "other_input" {} 3 | 4 | output "output" { 5 | value = "${var.input} ${var.other_input}" 6 | } -------------------------------------------------------------------------------- /examples/terragrunt/no-resources/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.4.2", 4 | "serial": 1, 5 | "lineage": "ca632d6d-4094-ee34-c5e1-73a326433142", 6 | "outputs": {}, 7 | "resources": [], 8 | "check_results": null 9 | } 10 | -------------------------------------------------------------------------------- /examples/terragrunt/no-resources/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | inputs = { 2 | input = "one input" 3 | other_input = "another input" 4 | extraneous_input = "an unused input" 5 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elementtech/terracove 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/GoogleCloudPlatform/testgrid v0.0.160 7 | github.com/ahmetb/go-linq/v3 v3.2.0 8 | github.com/briandowns/spinner v1.23.0 9 | github.com/daixiang0/gci v0.10.1 10 | github.com/go-critic/go-critic v0.7.0 11 | github.com/golangci/golangci-lint v1.51.2 12 | github.com/gotesttools/gotestfmt/v2 v2.4.1 13 | github.com/gruntwork-io/terratest v0.41.16 14 | github.com/hashicorp/terraform-json v0.16.0 15 | github.com/spf13/cobra v1.6.1 16 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 17 | golang.org/x/tools v0.7.0 18 | mvdan.cc/gofumpt v0.4.0 19 | ) 20 | 21 | require ( 22 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 23 | 4d63.com/gochecknoglobals v0.2.1 // indirect 24 | cloud.google.com/go v0.105.0 // indirect 25 | cloud.google.com/go/compute v1.12.1 // indirect 26 | cloud.google.com/go/compute/metadata v0.2.1 // indirect 27 | cloud.google.com/go/iam v0.7.0 // indirect 28 | cloud.google.com/go/storage v1.27.0 // indirect 29 | github.com/Abirdcfly/dupword v0.0.9 // indirect 30 | github.com/Antonboom/errname v0.1.7 // indirect 31 | github.com/Antonboom/nilnil v0.1.1 // indirect 32 | github.com/BurntSushi/toml v1.2.1 // indirect 33 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 34 | github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 // indirect 35 | github.com/Masterminds/semver v1.5.0 // indirect 36 | github.com/OpenPeeDeeP/depguard v1.1.1 // indirect 37 | github.com/agext/levenshtein v1.2.3 // indirect 38 | github.com/alexkohler/prealloc v1.0.0 // indirect 39 | github.com/alingse/asasalint v0.0.11 // indirect 40 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 41 | github.com/ashanbrown/forbidigo v1.4.0 // indirect 42 | github.com/ashanbrown/makezero v1.1.1 // indirect 43 | github.com/aws/aws-sdk-go v1.44.122 // indirect 44 | github.com/beorn7/perks v1.0.1 // indirect 45 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 46 | github.com/bkielbasa/cyclop v1.2.0 // indirect 47 | github.com/blizzy78/varnamelen v0.8.0 // indirect 48 | github.com/bombsimon/wsl/v3 v3.4.0 // indirect 49 | github.com/breml/bidichk v0.2.3 // indirect 50 | github.com/breml/errchkjson v0.3.0 // indirect 51 | github.com/butuzov/ireturn v0.1.1 // indirect 52 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 53 | github.com/charithe/durationcheck v0.0.9 // indirect 54 | github.com/chavacava/garif v0.0.0-20221024190013-b3ef35877348 // indirect 55 | github.com/cristalhq/acmd v0.11.1 // indirect 56 | github.com/curioswitch/go-reassign v0.2.0 // indirect 57 | github.com/davecgh/go-spew v1.1.1 // indirect 58 | github.com/denis-tingaikin/go-header v0.4.3 // indirect 59 | github.com/esimonov/ifshort v1.0.4 // indirect 60 | github.com/ettle/strcase v0.1.1 // indirect 61 | github.com/fatih/color v1.15.0 // indirect 62 | github.com/fatih/structtag v1.2.0 // indirect 63 | github.com/firefart/nonamedreturns v1.0.4 // indirect 64 | github.com/fsnotify/fsnotify v1.5.4 // indirect 65 | github.com/fzipp/gocyclo v0.6.0 // indirect 66 | github.com/go-toolsmith/astcast v1.1.0 // indirect 67 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 68 | github.com/go-toolsmith/astequal v1.1.0 // indirect 69 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 70 | github.com/go-toolsmith/astp v1.1.0 // indirect 71 | github.com/go-toolsmith/pkgload v1.2.2 // indirect 72 | github.com/go-toolsmith/strparse v1.1.0 // indirect 73 | github.com/go-toolsmith/typep v1.1.0 // indirect 74 | github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect 75 | github.com/gobwas/glob v0.2.3 // indirect 76 | github.com/gofrs/flock v0.8.1 // indirect 77 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 78 | github.com/golang/protobuf v1.5.2 // indirect 79 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect 80 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 81 | github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect 82 | github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 // indirect 83 | github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect 84 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect 85 | github.com/golangci/misspell v0.4.0 // indirect 86 | github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect 87 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect 88 | github.com/google/go-cmp v0.5.9 // indirect 89 | github.com/google/uuid v1.3.0 // indirect 90 | github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect 91 | github.com/googleapis/gax-go/v2 v2.7.0 // indirect 92 | github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect 93 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 94 | github.com/gostaticanalysis/comment v1.4.2 // indirect 95 | github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect 96 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 97 | github.com/hashicorp/errwrap v1.0.0 // indirect 98 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 99 | github.com/hashicorp/go-getter v1.7.1 // indirect 100 | github.com/hashicorp/go-multierror v1.1.1 // indirect 101 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 102 | github.com/hashicorp/go-version v1.6.0 // indirect 103 | github.com/hashicorp/hcl v1.0.0 // indirect 104 | github.com/hashicorp/hcl/v2 v2.9.1 // indirect 105 | github.com/hexops/gotextdiff v1.0.3 // indirect 106 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 107 | github.com/jgautheron/goconst v1.5.1 // indirect 108 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 109 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a // indirect 110 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect 111 | github.com/jmespath/go-jmespath v0.4.0 // indirect 112 | github.com/julz/importas v0.1.0 // indirect 113 | github.com/junk1tm/musttag v0.4.5 // indirect 114 | github.com/kisielk/errcheck v1.6.3 // indirect 115 | github.com/kisielk/gotool v1.0.0 // indirect 116 | github.com/kkHAIKE/contextcheck v1.1.3 // indirect 117 | github.com/klauspost/compress v1.15.11 // indirect 118 | github.com/kulti/thelper v0.6.3 // indirect 119 | github.com/kunwardeep/paralleltest v1.0.6 // indirect 120 | github.com/kyoh86/exportloopref v0.1.11 // indirect 121 | github.com/ldez/gomoddirectives v0.2.3 // indirect 122 | github.com/ldez/tagliatelle v0.4.0 // indirect 123 | github.com/leonklingele/grouper v1.1.1 // indirect 124 | github.com/lufeee/execinquery v1.2.1 // indirect 125 | github.com/magiconair/properties v1.8.6 // indirect 126 | github.com/maratori/testableexamples v1.0.0 // indirect 127 | github.com/maratori/testpackage v1.1.0 // indirect 128 | github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect 129 | github.com/mattn/go-colorable v0.1.13 // indirect 130 | github.com/mattn/go-isatty v0.0.18 // indirect 131 | github.com/mattn/go-runewidth v0.0.9 // indirect 132 | github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect 133 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 134 | github.com/mbilski/exhaustivestruct v1.2.0 // indirect 135 | github.com/mgechev/revive v1.2.5 // indirect 136 | github.com/mitchellh/go-homedir v1.1.0 // indirect 137 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 138 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 139 | github.com/mitchellh/mapstructure v1.5.0 // indirect 140 | github.com/moricho/tparallel v0.2.1 // indirect 141 | github.com/nakabonne/nestif v0.3.1 // indirect 142 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect 143 | github.com/nishanths/exhaustive v0.9.5 // indirect 144 | github.com/nishanths/predeclared v0.2.2 // indirect 145 | github.com/nunnatsa/ginkgolinter v0.8.1 // indirect 146 | github.com/olekukonko/tablewriter v0.0.5 // indirect 147 | github.com/pelletier/go-toml v1.9.5 // indirect 148 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 149 | github.com/pkg/errors v0.9.1 // indirect 150 | github.com/pmezard/go-difflib v1.0.0 // indirect 151 | github.com/polyfloyd/go-errorlint v1.1.0 // indirect 152 | github.com/prometheus/client_golang v1.12.1 // indirect 153 | github.com/prometheus/client_model v0.2.0 // indirect 154 | github.com/prometheus/common v0.32.1 // indirect 155 | github.com/prometheus/procfs v0.7.3 // indirect 156 | github.com/quasilyte/go-ruleguard v0.3.19 // indirect 157 | github.com/quasilyte/gogrep v0.5.0 // indirect 158 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 159 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 160 | github.com/ryancurrah/gomodguard v1.3.0 // indirect 161 | github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect 162 | github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect 163 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 164 | github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect 165 | github.com/securego/gosec/v2 v2.15.0 // indirect 166 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 167 | github.com/sirupsen/logrus v1.9.0 // indirect 168 | github.com/sivchari/containedctx v1.0.2 // indirect 169 | github.com/sivchari/nosnakecase v1.7.0 // indirect 170 | github.com/sivchari/tenv v1.7.1 // indirect 171 | github.com/sonatard/noctx v0.0.1 // indirect 172 | github.com/sourcegraph/go-diff v0.7.0 // indirect 173 | github.com/spf13/afero v1.8.2 // indirect 174 | github.com/spf13/cast v1.5.0 // indirect 175 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 176 | github.com/spf13/pflag v1.0.5 // indirect 177 | github.com/spf13/viper v1.12.0 // indirect 178 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 179 | github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect 180 | github.com/stretchr/objx v0.5.0 // indirect 181 | github.com/stretchr/testify v1.8.2 // indirect 182 | github.com/subosito/gotenv v1.4.1 // indirect 183 | github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect 184 | github.com/tdakkota/asciicheck v0.1.1 // indirect 185 | github.com/tetafro/godot v1.4.11 // indirect 186 | github.com/timakin/bodyclose v0.0.0-20221125081123-e39cf3fc478e // indirect 187 | github.com/timonwong/loggercheck v0.9.3 // indirect 188 | github.com/tmccombs/hcl2json v0.3.3 // indirect 189 | github.com/tomarrell/wrapcheck/v2 v2.8.0 // indirect 190 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 191 | github.com/ulikunitz/xz v0.5.10 // indirect 192 | github.com/ultraware/funlen v0.0.3 // indirect 193 | github.com/ultraware/whitespace v0.0.5 // indirect 194 | github.com/uudashr/gocognit v1.0.6 // indirect 195 | github.com/yagipy/maintidx v1.0.0 // indirect 196 | github.com/yeya24/promlinter v0.2.0 // indirect 197 | github.com/zclconf/go-cty v1.13.0 // indirect 198 | gitlab.com/bosi/decorder v0.2.3 // indirect 199 | go.opencensus.io v0.24.0 // indirect 200 | go.uber.org/atomic v1.10.0 // indirect 201 | go.uber.org/multierr v1.11.0 // indirect 202 | go.uber.org/zap v1.24.0 // indirect 203 | golang.org/x/crypto v0.5.0 // indirect 204 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect 205 | golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb // indirect 206 | golang.org/x/mod v0.9.0 // indirect 207 | golang.org/x/net v0.8.0 // indirect 208 | golang.org/x/oauth2 v0.1.0 // indirect 209 | golang.org/x/sync v0.1.0 // indirect 210 | golang.org/x/sys v0.7.0 // indirect 211 | golang.org/x/term v0.7.0 // indirect 212 | golang.org/x/text v0.8.0 // indirect 213 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 214 | google.golang.org/api v0.103.0 // indirect 215 | google.golang.org/appengine v1.6.7 // indirect 216 | google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c // indirect 217 | google.golang.org/grpc v1.51.0 // indirect 218 | google.golang.org/protobuf v1.28.1 // indirect 219 | gopkg.in/ini.v1 v1.67.0 // indirect 220 | gopkg.in/yaml.v2 v2.4.0 // indirect 221 | gopkg.in/yaml.v3 v3.0.1 // indirect 222 | honnef.co/go/tools v0.4.2 // indirect 223 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect 224 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect 225 | mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect 226 | ) 227 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | curl -L 'https://github.com/elementtech/terracove/releases/download/v0.0.7/terracove_0.0.7_Darwin_x86_64.tar.gz' | tar -xz terracove 2 | chmod u+x terracove 3 | mv terracove /usr/local/bin/terracove 4 | terracove --version -------------------------------------------------------------------------------- /internal/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | tfjson "github.com/hashicorp/terraform-json" 7 | // tfjson "github.com/hashicorp/terraform-json" 8 | ) 9 | 10 | type OutputOptions struct { 11 | Json bool 12 | Minimal bool 13 | HTML bool 14 | // Yaml bool 15 | Junit bool 16 | JsonOutPath string 17 | HTMLOutPath string 18 | // YamlOutPath string 19 | JunitOutPath string 20 | IgnoreError bool 21 | IgnoreEmpty bool 22 | } 23 | 24 | type RecursiveOptions struct { 25 | Exclude []string 26 | } 27 | 28 | type ValidateOptions struct { 29 | ValidateTerraformBy string 30 | ValidateTerragruntBy string 31 | } 32 | 33 | type TestReport struct { 34 | Name string 35 | Tests int 36 | Failures int 37 | Skipped int 38 | Errors int 39 | TestCases []TestCase 40 | } 41 | 42 | type TestCase struct { 43 | Name string 44 | Status string // "P" for passed, "F" for failed, "U" for untested 45 | Message string // error message if failed or skipped 46 | } 47 | 48 | type Result struct { 49 | Path string 50 | Error string 51 | ResourceCount uint 52 | ResourceCountExists uint 53 | ResourceCountDiff uint 54 | Coverage float64 55 | Duration time.Duration 56 | PlanJSON tfjson.Plan 57 | PlanRaw string 58 | 59 | ActionNoopCount uint 60 | ActionCreateCount uint 61 | ActionReadCount uint 62 | ActionUpdateCount uint 63 | ActionDeleteCount uint 64 | } 65 | 66 | type TerraformModuleStatus struct { 67 | Timestamp string 68 | Path string 69 | Results []Result 70 | Coverage float64 71 | } 72 | -------------------------------------------------------------------------------- /internal/types/types_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTypes(t *testing.T) { 11 | t.Run("TestReport", func(t *testing.T) { 12 | tr := TestReport{ 13 | Name: "test-report", 14 | Tests: 1, 15 | Failures: 1, 16 | Skipped: 0, 17 | Errors: 0, 18 | TestCases: []TestCase{ 19 | { 20 | Name: "test-case", 21 | Status: "F", 22 | Message: "expected failure", 23 | }, 24 | }, 25 | } 26 | 27 | assert.Equal(t, "test-report", tr.Name) 28 | assert.Equal(t, 1, tr.Tests) 29 | assert.Equal(t, 1, tr.Failures) 30 | assert.Equal(t, 0, tr.Skipped) 31 | assert.Equal(t, 0, tr.Errors) 32 | assert.Len(t, tr.TestCases, 1) 33 | assert.Equal(t, "test-case", tr.TestCases[0].Name) 34 | assert.Equal(t, "F", tr.TestCases[0].Status) 35 | assert.Equal(t, "expected failure", tr.TestCases[0].Message) 36 | }) 37 | 38 | t.Run("Result", func(t *testing.T) { 39 | r := Result{ 40 | Path: "/path/to/module", 41 | Error: "", 42 | ResourceCount: 10, 43 | ResourceCountExists: 5, 44 | ResourceCountDiff: 3, 45 | Coverage: 60.0, 46 | Duration: time.Second, 47 | // RawPlan: tfjson.Plan{}, 48 | ActionNoopCount: 1, 49 | ActionCreateCount: 2, 50 | ActionReadCount: 3, 51 | ActionUpdateCount: 4, 52 | ActionDeleteCount: 5, 53 | } 54 | 55 | assert.Equal(t, "/path/to/module", r.Path) 56 | assert.Empty(t, r.Error) 57 | assert.Equal(t, uint(10), r.ResourceCount) 58 | assert.Equal(t, uint(5), r.ResourceCountExists) 59 | assert.Equal(t, uint(3), r.ResourceCountDiff) 60 | assert.Equal(t, 60.0, r.Coverage) 61 | assert.Equal(t, time.Second, r.Duration) 62 | // assert.Equal(t, tfjson.Plan{}, r.RawPlan) 63 | assert.Equal(t, uint(1), r.ActionNoopCount) 64 | assert.Equal(t, uint(2), r.ActionCreateCount) 65 | assert.Equal(t, uint(3), r.ActionReadCount) 66 | assert.Equal(t, uint(4), r.ActionUpdateCount) 67 | assert.Equal(t, uint(5), r.ActionDeleteCount) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/elementtech/terracove/cmd" 8 | ) 9 | 10 | var version = "0.0.7" 11 | 12 | func main() { 13 | if err := cmd.Execute(version, false); err != nil { 14 | fmt.Fprintf(os.Stderr, "%v", err) 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/html/html.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | _ "embed" 5 | "html/template" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/elementtech/terracove/internal/types" 11 | ) 12 | 13 | func CreateHTML(suites []types.TerraformModuleStatus, path string) error { 14 | file, err := os.Create(path) 15 | 16 | if err != nil { 17 | return err 18 | } 19 | 20 | defer file.Close() 21 | tmpl, err := template.New("htmlTmpl").Funcs(template.FuncMap{ 22 | "strip": func(s string) string { 23 | var result strings.Builder 24 | for i := 0; i < len(s); i++ { 25 | b := s[i] 26 | if ('a' <= b && b <= 'z') || 27 | ('A' <= b && b <= 'Z') || 28 | ('0' <= b && b <= '9') || 29 | b == ' ' { 30 | result.WriteByte(b) 31 | } 32 | } 33 | return result.String() 34 | }, 35 | "formatTime": func(s string) string { 36 | t, err := time.Parse(time.RFC3339, s) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | humanReadable := t.Format("Monday, Jan 2, 2006 at 3:04pm") 42 | return humanReadable 43 | }, 44 | }).Parse(htmlTmpl) 45 | 46 | if err != nil { 47 | return err 48 | } 49 | 50 | err = tmpl.Execute(file, suites) 51 | 52 | if err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/html/template.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | var htmlTmpl string = ` 4 | 5 | 6 | 7 | 8 | 121 | 122 | {{range .}} 123 |
124 | {{ $timestamp := .Timestamp }} 125 | {{ $path := .Path }} 126 |
127 |
{{.Coverage}}% Coverage
128 |

{{$path}}

129 |
{{$timestamp |formatTime}}
130 |
131 |
132 | {{range .Results}} 133 |
135 | 136 | {{.Path}} 137 | 138 | {{if .Error}}Error{{else if (not (eq .ResourceCountDiff 0))}}Fail{{else if eq 139 | .ResourceCount 0}}Skip{{else}}Pass 140 | {{end}} 141 | 142 | 143 | 144 | 182 | 183 | 184 |
185 |
186 | 187 | {{.PlanRaw}} 188 | 189 |
190 |

{{.Duration}}

191 |
192 |
193 | {{end}} 194 |
195 | {{end}} 196 | 197 | 198 | 199 | 200 | 201 | ` 202 | -------------------------------------------------------------------------------- /pkg/report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "text/tabwriter" 10 | 11 | "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 12 | "github.com/ahmetb/go-linq/v3" 13 | "github.com/elementtech/terracove/internal/types" 14 | ) 15 | 16 | func CreateCoverageXML(suitesRoot junit.Suites, path string) error { 17 | file, err := os.Create(path) 18 | if err != nil { 19 | return err 20 | } 21 | defer file.Close() 22 | 23 | enc := xml.NewEncoder(file) 24 | enc.Indent("", "\t") 25 | if err := enc.Encode(suitesRoot); err != nil { 26 | return err 27 | } 28 | return nil 29 | } 30 | 31 | func CreateJson(suitesRoot []types.TerraformModuleStatus, path string) error { 32 | 33 | file, err := json.MarshalIndent(suitesRoot, "", " ") 34 | if err != nil { 35 | return err 36 | } 37 | 38 | err = ioutil.WriteFile(path, file, 0644) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // func CreateYaml(suitesRoot []types.TerraformModuleStatus, path string) error { 47 | 48 | // file, err := yaml.Marshal(suitesRoot) 49 | // if err != nil { 50 | // fmt.Println(err) 51 | // } 52 | 53 | // err = ioutil.WriteFile(path, file, 0644) 54 | // if err != nil { 55 | // fmt.Println(err) 56 | // } 57 | 58 | // return nil 59 | // } 60 | 61 | func CreateJunitStruct(terraformStatuses []types.TerraformModuleStatus) (junit.Suites, error) { 62 | suitesRoot := junit.Suites{} 63 | for _, ts := range terraformStatuses { 64 | suites := junit.Suite{ 65 | Name: ts.Path, 66 | Tests: len(ts.Results), 67 | Failures: linq.From(ts.Results).WhereT(func(r types.Result) bool { return r.ResourceCountDiff > 0 || r.Error != "" }).Count(), 68 | Time: linq.From(ts.Results).SelectT(func(r types.Result) float64 { return r.Duration.Seconds() }).Max().(float64), 69 | } 70 | 71 | for _, r := range ts.Results { 72 | 73 | testCase := junit.Result{ 74 | Name: r.Path, 75 | Time: r.Duration.Seconds(), 76 | } 77 | testCase.SetProperty("total", fmt.Sprint(r.ResourceCount)) 78 | testCase.SetProperty("diff", fmt.Sprint(r.ResourceCountDiff)) 79 | testCase.SetProperty("delete", fmt.Sprint(r.ActionDeleteCount)) 80 | testCase.SetProperty("update", fmt.Sprint(r.ActionUpdateCount)) 81 | testCase.SetProperty("read", fmt.Sprint(r.ActionReadCount)) 82 | testCase.SetProperty("create", fmt.Sprint(r.ActionCreateCount)) 83 | testCase.SetProperty("noop", fmt.Sprint(r.ActionNoopCount)) 84 | testCase.SetProperty("coverage", fmt.Sprint(r.Coverage)) 85 | switch { 86 | case r.Error != "": 87 | testCase.Errored = &junit.Errored{Message: "module has planning error"} 88 | testCase.Errored.Value = r.PlanRaw 89 | case r.ResourceCount == 0: 90 | testCase.Skipped = &junit.Skipped{Message: "module does not contain any resources"} 91 | testCase.Skipped.Value = r.PlanRaw 92 | case r.Coverage != 100: 93 | testCase.Failure = &junit.Failure{Message: fmt.Sprintf("module has %v resources with diff", r.ResourceCountDiff)} 94 | testCase.Failure.Value = r.PlanRaw 95 | } 96 | suites.Results = append(suites.Results, testCase) 97 | } 98 | suitesRoot.Suites = append(suitesRoot.Suites, suites) 99 | } 100 | 101 | return suitesRoot, nil 102 | } 103 | 104 | func CreateTestReport(testsuites junit.Suites) types.TestReport { 105 | var report types.TestReport 106 | report.Name = "\nTerraform Diff Report" 107 | report.Tests = 0 108 | report.Failures = 0 109 | report.Errors = 0 110 | report.Skipped = 0 111 | 112 | for _, suite := range testsuites.Suites { 113 | for _, tcase := range suite.Results { 114 | var tc types.TestCase 115 | tc.Name = tcase.Name 116 | if tcase.Failure != nil { 117 | tc.Status = "⚠️" 118 | tc.Message = tcase.Failure.Message 119 | report.Failures++ 120 | } else if tcase.Skipped != nil { 121 | tc.Status = "?" 122 | tc.Message = "Skipped" 123 | report.Skipped++ 124 | } else if tcase.Errored != nil { 125 | tc.Status = "X" 126 | tc.Message = tcase.Errored.Message 127 | report.Errors++ 128 | } else { 129 | tc.Status = "√" 130 | tc.Message = "Success" 131 | } 132 | report.TestCases = append(report.TestCases, tc) 133 | report.Tests++ 134 | } 135 | } 136 | return report 137 | } 138 | 139 | func PrettyPrinter(testsuites junit.Suites) { 140 | // Build the test report 141 | report := CreateTestReport(testsuites) 142 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) 143 | 144 | // Print the test report 145 | fmt.Fprintln(w, report.Name) 146 | fmt.Fprintln(w, "============================================================================") 147 | 148 | for _, tc := range report.TestCases { 149 | fmt.Fprintf(w, "%s\t\t%s\t\t\t%s\n", tc.Name, tc.Status, tc.Message) 150 | } 151 | w.Flush() 152 | 153 | fmt.Println("----------------------------------------------------------------------------") 154 | fmt.Printf("Test Results:\nFailed ⚠️%8d\nPassed √%8d\nSkipped ?%8d\nErrors X%8d\n", 155 | report.Failures, report.Tests-report.Failures-report.Skipped-report.Errors, report.Skipped, report.Errors) 156 | } 157 | -------------------------------------------------------------------------------- /pkg/report/report_test.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "io/ioutil" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 12 | "github.com/elementtech/terracove/internal/types" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestCreateCoverageXML(t *testing.T) { 17 | // Define test cases 18 | testCases := []struct { 19 | suitesRoot junit.Suites 20 | path string 21 | expectErr bool 22 | }{ 23 | { 24 | suitesRoot: junit.Suites{}, 25 | path: "../../tests/terracove.xml", 26 | expectErr: false, 27 | }, 28 | } 29 | 30 | // Run test cases 31 | for _, tc := range testCases { 32 | err := CreateCoverageXML(tc.suitesRoot, tc.path) 33 | 34 | // Verify result 35 | if tc.expectErr { 36 | assert.Error(t, err) 37 | } else { 38 | assert.NoError(t, err) 39 | assert.FileExists(t, tc.path) 40 | 41 | // Verify file content is valid XML 42 | file, err := os.Open(tc.path) 43 | assert.NoError(t, err) 44 | defer file.Close() 45 | 46 | dec := xml.NewDecoder(file) 47 | _, err = dec.Token() 48 | assert.NoError(t, err) 49 | } 50 | } 51 | } 52 | 53 | func TestCreateJson(t *testing.T) { 54 | // Define test cases 55 | testCases := []struct { 56 | suitesRoot []types.TerraformModuleStatus 57 | path string 58 | expectErr bool 59 | }{ 60 | { 61 | suitesRoot: []types.TerraformModuleStatus{}, 62 | path: "../../tests/terracove.json", 63 | expectErr: false, 64 | }, 65 | } 66 | 67 | // Run test cases 68 | for _, tc := range testCases { 69 | err := CreateJson(tc.suitesRoot, tc.path) 70 | 71 | // Verify result 72 | if tc.expectErr { 73 | assert.Error(t, err) 74 | } else { 75 | assert.NoError(t, err) 76 | assert.FileExists(t, tc.path) 77 | 78 | // Verify file content is valid JSON 79 | file, err := os.Open(tc.path) 80 | assert.NoError(t, err) 81 | defer file.Close() 82 | 83 | bytes, err := ioutil.ReadAll(file) 84 | assert.NoError(t, err) 85 | 86 | var data interface{} 87 | err = json.Unmarshal(bytes, &data) 88 | assert.NoError(t, err) 89 | } 90 | } 91 | } 92 | 93 | func TestCreateJunitStruct(t *testing.T) { 94 | // Create some sample data for testing 95 | terraformStatuses := []types.TerraformModuleStatus{ 96 | { 97 | Path: "module1", 98 | Results: []types.Result{ 99 | { 100 | Path: "resource1", 101 | Duration: time.Duration(1) * time.Second, 102 | ResourceCount: 2, 103 | ResourceCountDiff: 1, 104 | ActionDeleteCount: 0, 105 | ActionUpdateCount: 0, 106 | ActionReadCount: 0, 107 | ActionCreateCount: 1, 108 | ActionNoopCount: 0, 109 | Coverage: 50, 110 | }, 111 | }, 112 | }, 113 | } 114 | 115 | // Call the function being tested 116 | junitSuites, err := CreateJunitStruct(terraformStatuses) 117 | if err != nil { 118 | t.Errorf("CreateJunitStruct returned an error: %v", err) 119 | } 120 | 121 | // Verify the result 122 | if len(junitSuites.Suites) != 1 { 123 | t.Errorf("CreateJunitStruct returned %d suites, expected 1", len(junitSuites.Suites)) 124 | } 125 | suite := junitSuites.Suites[0] 126 | if suite.Name != "module1" { 127 | t.Errorf("CreateJunitStruct returned a suite with name %s, expected 'module1'", suite.Name) 128 | } 129 | if suite.Tests != 1 { 130 | t.Errorf("CreateJunitStruct returned a suite with %d tests, expected 1", suite.Tests) 131 | } 132 | if suite.Failures != 1 { 133 | t.Errorf("CreateJunitStruct returned a suite with %d failures, expected 1", suite.Failures) 134 | } 135 | result := suite.Results[0] 136 | if result.Name != "resource1" { 137 | t.Errorf("CreateJunitStruct returned a result with name %s, expected 'resource1'", result.Name) 138 | } 139 | if result.Time != 1.0 { 140 | t.Errorf("CreateJunitStruct returned a result with time %f, expected 1.0", result.Time) 141 | } 142 | } 143 | 144 | func TestPrettyPrinter(t *testing.T) { 145 | ts := junit.Suites{ 146 | Suites: []junit.Suite{ 147 | { 148 | Results: []junit.Result{ 149 | { 150 | Name: "Test case 1", 151 | Failure: &junit.Failure{ 152 | Message: "Assertion failed", 153 | }, 154 | }, 155 | { 156 | Name: "Test case 2", 157 | Skipped: &junit.Skipped{}, 158 | }, 159 | { 160 | Name: "Test case 3", 161 | Errored: &junit.Errored{ 162 | Message: "Unexpected error", 163 | }, 164 | }, 165 | { 166 | Name: "Test case 4", 167 | }, 168 | }, 169 | }, 170 | }, 171 | } 172 | 173 | PrettyPrinter(ts) 174 | } 175 | -------------------------------------------------------------------------------- /pkg/scan/scan.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "math" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | "github.com/briandowns/spinner" 15 | "github.com/elementtech/terracove/internal/types" 16 | "github.com/elementtech/terracove/pkg/html" 17 | "github.com/elementtech/terracove/pkg/report" 18 | "github.com/gruntwork-io/terratest/modules/logger" 19 | "github.com/gruntwork-io/terratest/modules/terraform" 20 | tfjson "github.com/hashicorp/terraform-json" 21 | "golang.org/x/exp/slices" 22 | ) 23 | 24 | func getAllDirectories(dirs []string, ValidateOptions types.ValidateOptions, RecursiveOptions types.RecursiveOptions) map[string][]string { 25 | subpaths := make(map[string][]string, len(dirs)) 26 | for _, dir := range dirs { 27 | filepath.WalkDir(dir, func(xpath string, xinfo fs.DirEntry, xerr error) error { 28 | if xerr != nil { 29 | fmt.Printf("error [%v] at a path [%q]\n", xerr, xpath) 30 | return xerr 31 | } 32 | if slices.Contains(RecursiveOptions.Exclude, filepath.Base(xpath)) { 33 | return filepath.SkipDir 34 | } 35 | if !xinfo.IsDir() { 36 | fmt.Printf("skipping file [%q]\n", xpath) 37 | return nil 38 | } 39 | if strings.HasPrefix(filepath.Base(xpath), ".") && xpath != "." { 40 | fmt.Printf("skipping file [%q]\n", xpath) 41 | return filepath.SkipDir 42 | } 43 | if moduleType := checkModuleType(xpath, ValidateOptions); moduleType != "" { 44 | subpaths[dir] = append(subpaths[dir], filepath.ToSlash(xpath)) 45 | } 46 | return nil 47 | }) 48 | } 49 | return subpaths 50 | } 51 | 52 | func checkModuleType(path string, ValidateOptions types.ValidateOptions) string { 53 | // Check for Terragrunt module 54 | if _, err := os.Stat(filepath.Join(path, ValidateOptions.ValidateTerragruntBy)); err == nil { 55 | return "terragrunt" 56 | } 57 | 58 | // Check for Terraform module 59 | if _, err := os.Stat(filepath.Join(path, ValidateOptions.ValidateTerraformBy)); err == nil { 60 | return "terraform" 61 | } 62 | 63 | // Module is not a Terraform or Terragrunt module 64 | return "" 65 | } 66 | 67 | func Flatten[T any](lists [][]T) []T { 68 | var res []T 69 | for _, list := range lists { 70 | res = append(res, list...) 71 | } 72 | return res 73 | } 74 | 75 | func TerraformModulesTerratest(paths []string, OutputOptions types.OutputOptions, ValidateOptions types.ValidateOptions, RecursiveOptions types.RecursiveOptions) error { 76 | dirsMap := getAllDirectories(paths, ValidateOptions, RecursiveOptions) 77 | timestamp := time.Now().Format(time.RFC3339) 78 | var statuses []types.TerraformModuleStatus 79 | var wg sync.WaitGroup 80 | fmt.Println(dirsMap) 81 | for root, v := range dirsMap { 82 | wg.Add(1) 83 | go func(root string, v []string) { 84 | defer wg.Done() 85 | 86 | var results []types.Result 87 | var mu sync.Mutex 88 | 89 | var wg2 sync.WaitGroup 90 | for _, dir := range v { 91 | wg2.Add(1) 92 | go func(dir string) { 93 | defer wg2.Done() 94 | moduleType := checkModuleType(dir, ValidateOptions) 95 | if moduleType == "" { 96 | return 97 | } 98 | tfOptions := &terraform.Options{ 99 | TerraformDir: dir, 100 | TerraformBinary: moduleType, 101 | Logger: logger.Discard, 102 | PlanFilePath: ".terracove.plan", 103 | } 104 | testingContext := testing.T{} 105 | now := time.Now() 106 | spinner := spinner.New(spinner.CharSets[33], 100*time.Millisecond) 107 | spinner.Suffix = fmt.Sprintf(" %v: ", dir) 108 | spinner.Start() 109 | output, err := terraform.InitAndPlanE(&testingContext, tfOptions) 110 | if OutputOptions.IgnoreError && (err != nil) { 111 | return 112 | } 113 | res := types.Result{ 114 | Path: dir, 115 | Duration: time.Since(now), 116 | } 117 | if err != nil { 118 | res.Error = err.Error() 119 | 120 | } 121 | if !OutputOptions.Minimal { 122 | if err != nil { 123 | res.PlanRaw = err.Error() 124 | } else { 125 | res.PlanRaw = output 126 | } 127 | } 128 | 129 | if res.Error == "" { 130 | plan, err := terraform.ShowWithStructE(&testingContext, tfOptions) 131 | if !OutputOptions.Minimal { 132 | if plan != nil { 133 | res.PlanJSON = plan.RawPlan 134 | } 135 | } 136 | if err == nil { 137 | resourceCount := len(plan.ResourceChangesMap) 138 | if OutputOptions.IgnoreEmpty && (resourceCount == 0) { 139 | return 140 | } 141 | var resourceCountExists uint 142 | var resourceCountDiff uint 143 | var actionCount = map[tfjson.Action]int{} 144 | for _, change := range plan.ResourceChangesMap { 145 | action := change.Change.Actions[0] 146 | if action == tfjson.ActionCreate || action == tfjson.ActionUpdate || action == tfjson.ActionDelete { 147 | resourceCountDiff++ 148 | } else { 149 | resourceCountExists++ 150 | } 151 | actionCount[action]++ 152 | } 153 | res.ResourceCount = uint(resourceCount) 154 | res.ResourceCountExists = resourceCountExists 155 | res.ResourceCountDiff = resourceCountDiff 156 | res.ActionNoopCount = uint(actionCount[tfjson.ActionNoop]) 157 | res.ActionCreateCount = uint(actionCount[tfjson.ActionCreate]) 158 | res.ActionReadCount = uint(actionCount[tfjson.ActionRead]) 159 | res.ActionUpdateCount = uint(actionCount[tfjson.ActionUpdate]) 160 | res.ActionDeleteCount = uint(actionCount[tfjson.ActionDelete]) 161 | res.Coverage = float64(percentage(float64(resourceCountExists), float64(resourceCount))) 162 | } 163 | 164 | } 165 | 166 | mu.Lock() 167 | results = append(results, res) 168 | mu.Unlock() 169 | }(dir) 170 | } 171 | wg2.Wait() 172 | mu.Lock() 173 | defer mu.Unlock() 174 | statuses = append(statuses, types.TerraformModuleStatus{ 175 | Path: root, 176 | Results: results, 177 | Timestamp: timestamp, 178 | Coverage: averagePercentage(results), 179 | }) 180 | }(root, v) 181 | } 182 | wg.Wait() 183 | junitStruct, err := report.CreateJunitStruct(statuses) 184 | if err != nil { 185 | fmt.Println(err) 186 | return err 187 | } 188 | if OutputOptions.Junit { 189 | 190 | if err := report.CreateCoverageXML(junitStruct, OutputOptions.JunitOutPath); err != nil { 191 | fmt.Println("Error while creating junit XML: ", err) 192 | return err 193 | } else { 194 | fmt.Printf("%v created succesfully\n", OutputOptions.JunitOutPath) 195 | } 196 | } 197 | if OutputOptions.Json { 198 | if err := report.CreateJson(statuses, OutputOptions.JsonOutPath); err != nil { 199 | fmt.Println("Error while creating JSON: ", err) 200 | return err 201 | } else { 202 | fmt.Printf("%v created succesfully\n", OutputOptions.JsonOutPath) 203 | } 204 | } 205 | if OutputOptions.HTML { 206 | if err := html.CreateHTML(statuses, OutputOptions.HTMLOutPath); err != nil { 207 | fmt.Println("Error while creating JSON: ", err) 208 | return err 209 | } else { 210 | fmt.Printf("%v created succesfully\n", OutputOptions.HTMLOutPath) 211 | } 212 | } 213 | // if OutputOptions.Yaml { 214 | // if err := report.CreateYaml(statuses, OutputOptions.YamlOutPath); err != nil { 215 | // fmt.Println("Error while creating YAML: ", err) 216 | // } else { 217 | // fmt.Printf("%v created succesfully\n", OutputOptions.YamlOutPath) 218 | // } 219 | // } 220 | report.PrettyPrinter(junitStruct) 221 | return nil 222 | } 223 | 224 | func percentage(num float64, denom float64) float64 { 225 | if denom == 0 { 226 | return 100 227 | } 228 | return math.Round((num/denom)*10000) / 100 229 | } 230 | 231 | func averagePercentage(results []types.Result) float64 { 232 | var percentages []float64 233 | 234 | for _, p := range results { 235 | percentages = append(percentages, p.Coverage) 236 | } 237 | 238 | sum := 0.0 239 | 240 | for _, p := range percentages { 241 | sum += p 242 | } 243 | 244 | if len(percentages) > 0 { 245 | return math.Round((sum/float64(len(percentages)))*100) / 100 246 | } 247 | 248 | return 0 249 | } 250 | -------------------------------------------------------------------------------- /pkg/scan/scan_test.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/elementtech/terracove/internal/types" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestGetAllDirectories(t *testing.T) { 17 | path, err := os.Getwd() 18 | if err != nil { 19 | log.Println(err) 20 | } 21 | fmt.Println(path) // for example /home/user 22 | // Test getAllDirectories function 23 | testDir := "../../examples" 24 | validateOptions := types.ValidateOptions{ValidateTerraformBy: "main.tf", ValidateTerragruntBy: "terragrunt.hcl"} 25 | recursiveOptions := types.RecursiveOptions{Exclude: []string{"error"}} 26 | subpaths := getAllDirectories([]string{testDir}, validateOptions, recursiveOptions) 27 | 28 | expectedResult := map[string][]string{ 29 | testDir: {filepath.ToSlash(testDir + "/terraform/success"), filepath.ToSlash(testDir + "/terraform/tfstate-diff"), filepath.ToSlash(testDir + "/terragrunt/no-resources")}, 30 | } 31 | 32 | assert.Equal(t, expectedResult, subpaths) 33 | 34 | assert.Empty(t, nil) 35 | } 36 | 37 | func TestCheckModuleType(t *testing.T) { 38 | // Create temporary directory for testing 39 | dir := t.TempDir() 40 | 41 | // Create test files 42 | terrFile := filepath.Join(dir, "terragrunt.hcl") 43 | terraformFile := filepath.Join(dir, "main.tf") 44 | os.Create(terrFile) 45 | os.Create(terraformFile) 46 | 47 | // Define ValidateOptions 48 | opts := types.ValidateOptions{ 49 | ValidateTerragruntBy: "terragrunt.hcl", 50 | ValidateTerraformBy: "main.tf", 51 | } 52 | 53 | // Test for Terragrunt module 54 | if moduleType := checkModuleType(dir, opts); moduleType != "terragrunt" { 55 | t.Errorf("Expected module type 'terragrunt', but got '%s'", moduleType) 56 | } 57 | 58 | // Test for Terraform module 59 | os.Remove(terrFile) 60 | if moduleType := checkModuleType(dir, opts); moduleType != "terraform" { 61 | t.Errorf("Expected module type 'terraform', but got '%s'", moduleType) 62 | } 63 | 64 | // Test for non-Terraform/Terragrunt module 65 | os.Remove(terraformFile) 66 | if moduleType := checkModuleType(dir, opts); moduleType != "" { 67 | t.Errorf("Expected empty module type, but got '%s'", moduleType) 68 | } 69 | } 70 | 71 | func TestFlatten(t *testing.T) { 72 | // Test case 1 73 | input1 := [][]int{{1, 2, 3}, {4, 5}, {6, 7, 8, 9}} 74 | expected1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 75 | if res := Flatten(input1); !equal(res, expected1) { 76 | t.Errorf("Flatten(%v) = %v; expected %v", input1, res, expected1) 77 | } 78 | 79 | // Test case 2 80 | input2 := [][]string{{"foo", "bar"}, {"baz", "qux", "quux"}} 81 | expected2 := []string{"foo", "bar", "baz", "qux", "quux"} 82 | if res := Flatten(input2); !equal(res, expected2) { 83 | t.Errorf("Flatten(%v) = %v; expected %v", input2, res, expected2) 84 | } 85 | } 86 | 87 | // Helper function to check if two slices are equal 88 | func equal[T comparable](a, b []T) bool { 89 | if len(a) != len(b) { 90 | return false 91 | } 92 | for i, v := range a { 93 | if v != b[i] { 94 | return false 95 | } 96 | } 97 | return true 98 | } 99 | 100 | func TestPercentage(t *testing.T) { 101 | // Test case 1: denominator is zero 102 | res1 := percentage(50.0, 0.0) 103 | require.Equal(t, float64(100), res1) 104 | 105 | // Test case 2: numerator is zero 106 | res2 := percentage(0.0, 10.0) 107 | require.Equal(t, float64(0), res2) 108 | 109 | // Test case 3: normal case 110 | res3 := percentage(6.0, 10.0) 111 | require.Equal(t, float64(60), res3) 112 | 113 | // Test case 4: rounding to two decimal places 114 | res4 := percentage(5.0, 6.0) 115 | require.Equal(t, float64(83.33), math.Round(res4*100)/100) 116 | } 117 | 118 | func TestAveragePercentage(t *testing.T) { 119 | // Test case 1: empty slice 120 | res1 := averagePercentage([]types.Result{}) 121 | require.Equal(t, float64(0), res1) 122 | 123 | // Test case 2: normal case 124 | results := []types.Result{ 125 | {Coverage: 50}, 126 | {Coverage: 75}, 127 | {Coverage: 80}, 128 | } 129 | res2 := averagePercentage(results) 130 | require.Equal(t, float64(68.33), math.Round(res2*100)/100) 131 | } 132 | 133 | func TestTerraformModulesTerratest(t *testing.T) { 134 | // Define some example input values 135 | paths := []string{"../../examples"} 136 | outputOptions := types.OutputOptions{Junit: true, HTML: false, JunitOutPath: "../../test.xml", Json: true, JsonOutPath: "../../test.json", HTMLOutPath: "../../test.html"} 137 | validateOptions := types.ValidateOptions{ 138 | ValidateTerragruntBy: "terragrunt.hcl", 139 | ValidateTerraformBy: "main.tf", 140 | } 141 | 142 | recursiveOptions := types.RecursiveOptions{Exclude: []string{}} 143 | 144 | // Call the function being tested 145 | result := TerraformModulesTerratest(paths, outputOptions, validateOptions, recursiveOptions) 146 | os.Remove("../../test.xml") 147 | os.Remove("../../test.json") 148 | // os.Remove("../../test.html") 149 | // Check that the actual result matches the expected output 150 | assert.Nil(t, result) 151 | } 152 | -------------------------------------------------------------------------------- /tests/terracove.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /tests/terracove.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 6 | 7 | //go:generate go install github.com/golangci/golangci-lint/cmd/golangci-lint 8 | //go:generate go install mvdan.cc/gofumpt 9 | //go:generate go install github.com/daixiang0/gci 10 | //go:generate go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt 11 | //go:generate go install golang.org/x/tools/cmd/goimports 12 | //go:generate go install golang.org/x/lint/golint 13 | //go:generate go install github.com/go-critic/go-critic/cmd/gocritic 14 | 15 | // nolint 16 | import ( 17 | // gci 18 | _ "github.com/daixiang0/gci" 19 | // gocritic 20 | _ "github.com/go-critic/go-critic/cmd/gocritic" 21 | // golangci-lint 22 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 23 | // gotestfmt 24 | _ "github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt" 25 | // golint 26 | _ "golang.org/x/lint/golint" 27 | // goimports 28 | _ "golang.org/x/tools/cmd/goimports" 29 | // gofumpt 30 | _ "mvdan.cc/gofumpt" 31 | ) 32 | --------------------------------------------------------------------------------