├── .coderabbit.yaml ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yaml │ ├── codeql.yml │ ├── terrafetch.yaml │ └── test.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .pre-commit-config.yaml ├── Brewfile ├── FUNDING.yml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── action.sh ├── action.yml ├── cmd └── root.go ├── docs ├── img │ ├── terrafetch-demo.gif │ └── terrafetch-logo.png └── terrafetch-demo.tape ├── go.mod ├── go.sum ├── internal ├── analyze.go └── error.go ├── main.go ├── pkg ├── tui │ ├── logo.go │ └── ui.go └── utils │ └── dirs.go ├── renovate.json └── tests ├── account-map ├── README.md ├── modules │ ├── iam-roles │ │ ├── README.md │ │ ├── context.tf │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── providers.tf │ │ └── versions.tf │ └── team-assume-role-policy │ │ ├── README.md │ │ ├── context.tf │ │ ├── github-assume-role-policy.mixin.tf │ │ ├── main.tf │ │ └── versions.tf ├── outputs.tf └── versions.tf ├── api ├── main.tf └── providers.tf ├── cache ├── main.tf └── providers.tf ├── cdn ├── main.tf └── providers.tf ├── cluster ├── main.tf └── providers.tf ├── database ├── main.tf └── providers.tf ├── dynamodb ├── README.md ├── component.yaml ├── context.tf ├── main.tf ├── outputs.tf ├── providers.tf ├── variables.tf └── versions.tf ├── frontend ├── main.tf └── providers.tf ├── github-oidc-provider ├── README.md ├── outputs.tf └── versions.tf ├── github-oidc-role ├── README.md ├── additional-policy-map.tf ├── component.yaml ├── context.tf ├── main.tf ├── outputs.tf ├── policy_gitops.tf ├── policy_lambda-cicd.tf ├── providers.tf ├── variables.tf └── versions.tf ├── load-balancer ├── main.tf └── providers.tf ├── object-storage ├── main.tf └── providers.tf ├── s3-bucket ├── README.md ├── component.yaml ├── context.tf ├── main.tf ├── outputs.tf ├── providers.tf ├── remote-state.tf ├── variables.tf └── versions.tf ├── small └── main.tf └── vpc ├── main.tf └── providers.tf /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json 2 | language: "en-US" 3 | tone_instructions: "Maintain a conversational spartan tone that is supportive in all responses." 4 | early_access: true 5 | enable_free_tier: true 6 | reviews: 7 | profile: "chill" 8 | request_changes_workflow: true 9 | high_level_summary: true 10 | high_level_summary_placeholder: "@auto-summary" 11 | auto_title_placeholder: "@auto-title" 12 | review_status: false 13 | commit_status: false 14 | poem: false 15 | collapse_walkthrough: true 16 | sequence_diagrams: true 17 | changed_files_summary: true 18 | labeling_instructions: 19 | - label: "patch" 20 | instructions: "Apply when the PR contains changes that fix existing functionality." 21 | - label: "minor" 22 | instructions: "Apply when changes introduce new functionality but are non-breaking." 23 | - label: "major" 24 | instructions: "Apply when changes introduce new functionality that causes breaking changes requiring the user to update their configuration." 25 | - label: "no-release" 26 | instructions: "Apply this label when the PR contains changes only to documentation, website content, or other non-Go code. Changes that do not affect the Go code or the application functionality fall under this category." 27 | abort_on_close: true 28 | auto_review: 29 | enabled: true 30 | auto_incremental_review: true 31 | ignore_title_keywords: 32 | - "WIP" 33 | - "test" 34 | drafts: false 35 | base_branches: 36 | - "main" 37 | tools: 38 | shellcheck: 39 | enabled: true 40 | markdownlint: 41 | enabled: true 42 | github-checks: 43 | enabled: true 44 | timeout_ms: 90000 45 | languagetool: 46 | enabled: true 47 | enabled_rules: 48 | - "EN_QUOTES" 49 | level: "picky" 50 | hadolint: 51 | enabled: true 52 | yamllint: 53 | enabled: true 54 | gitleaks: 55 | enabled: true 56 | checkov: 57 | enabled: true 58 | chat: 59 | auto_reply: true 60 | knowledge_base: 61 | opt_out: false 62 | learnings: 63 | scope: "auto" 64 | issues: 65 | scope: "auto" 66 | linear: 67 | team_keys: 68 | - "DEV" 69 | pull_requests: 70 | scope: "auto" 71 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | insert_final_newline = true 3 | 4 | # Override for Makefile 5 | [{Makefile,makefile,GNUmakefile}] 6 | indent_style = tab 7 | indent_size = 4 8 | 9 | [Makefile.*] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [*.md] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.{tf,tfvars,tpl}] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @RoseSecurity 2 | -------------------------------------------------------------------------------- /.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 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.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/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Why 2 | 3 | 7 | 8 | ## What 9 | 10 | 15 | 16 | ## References 17 | 18 | 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "Build CLI and Attach to GitHub Release" 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | 13 | jobs: 14 | build: 15 | name: "Build CLI and Attach to GitHub Release" 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v5 19 | with: 20 | fetch-depth: 0 21 | 22 | # Setup Go 23 | - name: "Setup Go" 24 | uses: actions/setup-go@v6 25 | with: 26 | go-version-file: "go.mod" 27 | 28 | # Print Go version 29 | - run: go version 30 | 31 | # Build and release 32 | - name: Run GoReleaser 33 | uses: goreleaser/goreleaser-action@v6 34 | with: 35 | version: latest 36 | args: release --parallelism 2 --clean 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 39 | 40 | - name: Get Debian Package Names 41 | id: deb_package 42 | run: | 43 | echo "ARM_PACKAGE=$(find dist/ -name '*arm64.deb' | head -n 1)" >> $GITHUB_ENV 44 | echo "AMD_PACKAGE=$(find dist/ -name '*amd64.deb' | head -n 1)" >> $GITHUB_ENV 45 | 46 | # Push the Debian package to Cloudsmith 47 | - name: Push Debian ARM package to Cloudsmith 48 | id: push_arm 49 | uses: cloudsmith-io/action@master 50 | with: 51 | api-key: ${{ secrets.CLOUDSMITH_API_KEY }} 52 | command: "push" 53 | format: "deb" 54 | owner: "rosesecurity" 55 | repo: "terrafetch" 56 | distro: "any-distro" 57 | release: "any-version" 58 | file: ${{ env.ARM_PACKAGE }} 59 | 60 | - name: Push Debian AMD package to Cloudsmith 61 | id: push_amd 62 | uses: cloudsmith-io/action@master 63 | with: 64 | api-key: ${{ secrets.CLOUDSMITH_API_KEY }} 65 | command: "push" 66 | format: "deb" 67 | owner: "rosesecurity" 68 | repo: "terrafetch" 69 | distro: "any-distro" 70 | release: "any-version" 71 | file: ${{ env.AMD_PACKAGE }} 72 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Advanced" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '30 20 * * 6' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze (${{ matrix.language }}) 14 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 15 | permissions: 16 | # required for all workflows 17 | security-events: write 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | include: 23 | - language: go 24 | build-mode: autobuild 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v5 29 | 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v4 32 | with: 33 | languages: ${{ matrix.language }} 34 | build-mode: ${{ matrix.build-mode }} 35 | 36 | - if: matrix.build-mode == 'manual' 37 | shell: bash 38 | run: | 39 | echo 'If you are using a "manual" build mode for one or more of the' \ 40 | 'languages you are analyzing, replace this with the commands to build' \ 41 | 'your code, for example:' 42 | echo ' make bootstrap' 43 | echo ' make release' 44 | exit 1 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v4 48 | with: 49 | category: "/language:${{matrix.language}}" 50 | -------------------------------------------------------------------------------- /.github/workflows/terrafetch.yaml: -------------------------------------------------------------------------------- 1 | name: Terrafetch 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | terrafetch: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v5 16 | with: 17 | fetch-depth: 0 18 | - uses: RoseSecurity/terrafetch@main 19 | id: terrafetch 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test Terrafetch 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Harden the runner (Audit all outbound calls) 16 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 17 | with: 18 | egress-policy: audit 19 | 20 | - name: Checkout repository 21 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 22 | 23 | - name: Setup Terraform 24 | uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 25 | 26 | - name: Setup Go 27 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 28 | with: 29 | go-version-file: "go.mod" 30 | cache: false 31 | 32 | - name: Build and test Terrafetch 33 | run: | 34 | echo "Building Terrafetch..." 35 | make build 36 | 37 | echo "Running Terrafetch..." 38 | build/terrafetch > output.txt 39 | 40 | # Validate that expected elements are present 41 | echo "Validating output..." 42 | grep -q "Terraform Files:" output.txt || { echo "Missing Terraform Files count"; exit 1; } 43 | grep -q "Resources:" output.txt || { echo "Missing Resources count"; exit 1; } 44 | grep -q "Variables:" output.txt || { echo "Missing Variables count"; exit 1; } 45 | 46 | echo "✅ Validation passed: Output contains expected elements" 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | build/** 26 | .env 27 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - errcheck 6 | - goconst 7 | - gocritic 8 | - gosec 9 | - govet 10 | - ineffassign 11 | - misspell 12 | - nolintlint 13 | - staticcheck 14 | - unconvert 15 | - unused 16 | exclusions: 17 | generated: lax 18 | presets: 19 | - comments 20 | - common-false-positives 21 | - legacy 22 | - std-error-handling 23 | paths: 24 | - third_party$ 25 | - builtin$ 26 | - examples$ 27 | formatters: 28 | enable: 29 | - gofmt 30 | - goimports 31 | exclusions: 32 | generated: lax 33 | paths: 34 | - third_party$ 35 | - builtin$ 36 | - examples$ 37 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # Make sure to check the documentation at https://goreleaser.com 2 | version: 2 3 | 4 | before: 5 | hooks: 6 | - go mod tidy 7 | 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - freebsd 14 | - windows 15 | - darwin 16 | 17 | nfpms: 18 | - id: deb 19 | formats: 20 | - deb 21 | file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}" 22 | maintainer: RoseSecurity 23 | description: |- 24 | The Neofetch of Terraform. 25 | dependencies: 26 | - bash 27 | contents: 28 | - src: ./dist/{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}.deb 29 | dst: /usr/local/bin/{{ .ProjectName }} 30 | homepage: "https://github.com/RoseSecurity/terrafetch" 31 | license: Apache 2.0 32 | 33 | archives: 34 | - id: tar 35 | formats: 36 | - tar.gz 37 | name_template: >- 38 | {{ .ProjectName }}_ 39 | {{- title .Os }}_ 40 | {{- if eq .Arch "amd64" }}x86_64 41 | {{- else if eq .Arch "386" }}i386 42 | {{- else }}{{ .Arch }}{{ end }} 43 | {{- if .Arm }}v{{ .Arm }}{{ end }} 44 | - id: binary 45 | formats: 46 | - binary 47 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 48 | format_overrides: 49 | - goos: windows 50 | formats: [zip] 51 | 52 | changelog: 53 | sort: asc 54 | filters: 55 | exclude: 56 | - "^docs:" 57 | - "^test:" 58 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: gofumpt 5 | name: gofumpt 6 | entry: bash -c 'make fmt' 7 | language: system 8 | types: ["go"] 9 | files: main.go|cmd\/.*|internal\/.* 10 | pass_filenames: false 11 | - repo: local 12 | hooks: 13 | - id: rebuild-docs 14 | name: rebuild-docs 15 | entry: bash -c 'make docs' 16 | language: system 17 | files: cmd\/.* 18 | pass_filenames: false 19 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "go" 2 | brew "gofumpt" 3 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rosesecurity 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME=terrafetch 2 | VERSION=local 3 | GO=go 4 | 5 | default: help 6 | 7 | help: ## List Makefile targets 8 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 9 | 10 | all: build 11 | 12 | fmt: ## Format Go files 13 | gofumpt -w . 14 | 15 | build: ## Build Terrafetch 16 | env $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) $(GO) build -o build/$(BINARY_NAME) main.go 17 | 18 | deps: ## Download dependencies 19 | go mod download 20 | 21 | get: ## Install dependencies 22 | go get 23 | 24 | clean: ## Clean up build artifacts 25 | $(GO) clean 26 | rm ./build/$(BINARY_NAME) 27 | 28 | generate: ## Generate tooling licensing 29 | cd tools; go generate ./... 30 | 31 | testacc: ## Run acceptance tests 32 | go test ./... 33 | 34 | run: build ## Run Terrafetch 35 | ./build/$(BINARY_NAME) 36 | 37 | docs: build ## Generate documentation 38 | ./build/$(BINARY_NAME) docs 39 | 40 | version: build ## View binary version 41 | chmod +x ./build/$(BINARY_NAME) 42 | ./build/$(BINARY_NAME) version 43 | 44 | .PHONY: all build install clean run fmt help 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | terrafetch-logo 3 | 4 |

5 | Let your IaC flex for you. 6 |

7 | 8 | ## Introduction 9 | 10 | Terrafetch is the Neofetch of Terraform—because your infrastructure deserves a little flair. It scans your Terraform repository and displays key statistics like the number of variables, resources, modules, outputs, and more—all in a stylish, terminal-friendly format. Perfect for CLI screenshots, repo intros, or just flexing your infra hygiene. 11 | 12 | ## Demo 13 | 14 | ![terrafetch-demo](https://github.com/user-attachments/assets/ae3c34d2-db4b-430f-830e-44b0601f091a) 15 | 16 | ## Installation 17 | 18 | ### Go 19 | 20 | If you have a functional Go environment, you can install with: 21 | 22 | ```sh 23 | go install github.com/RoseSecurity/terrafetch@latest 24 | ``` 25 | 26 | ### Apt 27 | 28 | To install packages, you can quickly setup the repository automatically: 29 | 30 | ```sh 31 | curl -1sLf \ 32 | 'https://dl.cloudsmith.io/public/rosesecurity/terrafetch/setup.deb.sh' \ 33 | | sudo -E bash 34 | ``` 35 | 36 | Once the repository is configured, you can install with: 37 | 38 | ```sh 39 | apt install terrafetch 40 | ``` 41 | 42 | ### Source 43 | 44 | ```sh 45 | git clone git@github.com:RoseSecurity/terrafetch.git 46 | cd terrafetch 47 | make build 48 | ``` 49 | 50 | ## Usage 51 | 52 | > [!IMPORTANT] 53 | > Do you love the tool but it's missing some information you'd like to see? Head on over to [this discussion](https://github.com/RoseSecurity/terrafetch/discussions/2) and drop a comment or open a new issue! 54 | 55 | ```sh 56 | ⨠ terrafetch 57 | ╭─────────────────────────────────────────────────────────────────╮ 58 | │ . │ 59 | │@# - │ 60 | │@@@@@ Terraform Files: 1315 │ 61 | │@@@@@@@@. Documentation: 192 │ 62 | │@@@@@@@@@@ + # Providers: 334 │ 63 | │@@@@@@@@@@ @@@@ @@@@ Module Calls: 748 │ 64 | │@@@@@@@@@@ @@@@@@@. .@@@@@@@ Resources: 424 │ 65 | │ @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ Data Sources: 288 │ 66 | │ +@@@@@ @@@@@@@@@@ @@@@@@@@@@ Variables: 6122 │ 67 | │ .@@ @@@@@@@@@@ @@@@@@@@@@ Sensitive Variables: 16 │ 68 | │ @@@@@@@@@@ @@@@@@@@@@ Outputs: 807 │ 69 | │ @+ -@@@@@@ @@@@@@= Sensitive Outputs: 22 │ 70 | │ @@@@@ .@@@ @@@. │ 71 | │ @@@@@@@@. │ 72 | │ @@@@@@@@@@ │ 73 | │ @@@@@@@@@@ │ 74 | │ @@@@@@@@@@ │ 75 | │ .@@@@@@@@ │ 76 | │ @@@@@ │ 77 | │ %@ │ 78 | │ │ 79 | ╰─────────────────────────────────────────────────────────────────╯ 80 | ``` 81 | 82 | ## GitHub Action 83 | 84 | Give your infrastructure repositories some flair by injecting Terrafetch statistics right into your documentation. 85 | 86 | 1. Add report markers somewhere in your `README.md` (or any file you point the action at): 87 | 88 | ```console 89 | 90 | 91 | ``` 92 | 93 | 2. Make sure your repo permissions allow the default `GITHUB_TOKEN` to `contents: write` so the bot can push the updated file. 94 | 95 | ### Example Workflow 96 | 97 | ```yaml 98 | name: Terrafetch 99 | 100 | on: 101 | schedule: 102 | - cron: "0 3 * * *" # every night at 03:00 103 | workflow_dispatch: # manual trigger when you need it 104 | 105 | permissions: 106 | contents: write # let the action push changes 107 | 108 | jobs: 109 | terrafetch: 110 | runs-on: ubuntu-latest 111 | 112 | steps: 113 | - uses: actions/checkout@v4 114 | with: 115 | fetch-depth: 0 116 | 117 | - name: Generate README stats with Terrafetch 118 | uses: RoseSecurity/terrafetch@v0.3.0 119 | with: 120 | terraform_directory: infra 121 | output_file: README.md # file with the START/END markers 122 | terrafetch_version: 0.3.0 # "latest" also works 123 | ``` 124 | 125 | 3. Enjoy your new and improved documentation (as you can see here) 126 | 127 | 128 |
Terrafetch 129 | 130 | ```console 131 | ╭────────────────────────────────────────────────────────────────╮ 132 | │ . │ 133 | │@# - │ 134 | │@@@@@ Terraform Files: 54 │ 135 | │@@@@@@@@. Documentation: 8 │ 136 | │@@@@@@@@@@ + # Providers: 16 │ 137 | │@@@@@@@@@@ @@@@ @@@@ Module Calls: 19 │ 138 | │@@@@@@@@@@ @@@@@@@. .@@@@@@@ Resources: 11 │ 139 | │ @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ Data Sources: 7 │ 140 | │ +@@@@@ @@@@@@@@@@ @@@@@@@@@@ Variables: 191 │ 141 | │ .@@ @@@@@@@@@@ @@@@@@@@@@ Sensitive Variables: 1 │ 142 | │ @@@@@@@@@@ @@@@@@@@@@ Outputs: 43 │ 143 | │ @+ -@@@@@@ @@@@@@= Sensitive Outputs: 1 │ 144 | │ @@@@@ .@@@ @@@. │ 145 | │ @@@@@@@@. │ 146 | │ @@@@@@@@@@ │ 147 | │ @@@@@@@@@@ │ 148 | │ @@@@@@@@@@ │ 149 | │ .@@@@@@@@ │ 150 | │ @@@@@ │ 151 | │ %@ │ 152 | │ │ 153 | ╰────────────────────────────────────────────────────────────────╯ 154 | ``` 155 |
156 | 157 | 158 | ## Contributing 159 | 160 | For bug reports & feature requests, please use the [issue tracker](https://github.com/rosesecurity/terrafetch/issues). 161 | 162 | PRs are welcome! We follow the typical "fork-and-pull" Git workflow. 163 | 1. **Fork** the repo on GitHub 164 | 2. **Clone** the project to your own machine 165 | 3. **Commit** changes to your own branch 166 | 4. **Push** your work back up to your fork 167 | 5. Submit a **Pull Request** so that we can review your changes 168 | 169 | > [!TIP] 170 | > Be sure to merge the latest changes from "upstream" before making a pull request! 171 | 172 | ### Many Thanks to Our Contributors 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you believe you have found a security vulnerability in any repository owned by RoseSecurity, please let me know straight away. I will investigate all legitimate reports and do my best to quickly fix the problem. 6 | 7 | ### What to Include in Your Report 8 | 9 | To help me better understand the nature and scope of the issue, please include as much of the following information as possible in your report: 10 | 11 | - Description of the vulnerability and its potential impact. 12 | - Step-by-step instructions to reproduce the issue. 13 | - Affected versions and configurations. 14 | - Any possible mitigations or workarounds that you have identified. 15 | 16 | ### What to Expect 17 | 18 | > [!NOTE] 19 | > **Bug Bounties** 20 | > 21 | > RoseSecurity **does not** provide bug bounties for vulnerability disclosures. 22 | > 23 | > As an open-source contributor, I release projects for free under a permissive license, encouraging community contributions. 24 | > 25 | 26 | After you submit a report, I will: 27 | - Respond to your report within 48 hours to acknowledge receipt. 28 | - Provide an estimated time frame for addressing the vulnerability. 29 | - Notify you when the issue is resolved. 30 | -------------------------------------------------------------------------------- /action.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | # Detect platform 5 | if [[ "$(uname -s)" != "Linux" ]]; then 6 | echo "This action currently supports Ubuntu-linux runners only."; exit 1 7 | fi 8 | 9 | # Install terrafetch 10 | echo "::group::Install terrafetch" 11 | ver="${INPUT_TERRAFETCH_VERSION}" 12 | if [[ "$ver" == "latest" ]]; then ver=""; fi 13 | curl -1sLf https://dl.cloudsmith.io/public/rosesecurity/terrafetch/setup.deb.sh \ 14 | | sudo -E bash 15 | sudo apt-get -qq update 16 | sudo apt-get -qq install -y "terrafetch${ver:+=$ver}" 17 | echo "::endgroup::" 18 | 19 | # Run terrafetch 20 | echo "::group::Run terrafetch" 21 | set +e 22 | TERRAFETCH_OUTPUT="$(terrafetch -d "$INPUT_TERRAFORM_DIRECTORY")" 23 | ret=$? 24 | set -e 25 | echo "terrafetch finished with exit code $ret" 26 | echo "terrafetch-return-code=$ret" >>"$GITHUB_OUTPUT" 27 | [[ $ret -ne 0 ]] && exit "$ret" # fail the step/job on error 28 | echo "::endgroup::" 29 | 30 | # Inject output into README 31 | outfile="$INPUT_OUTPUT_FILE" 32 | tmp=$(mktemp) 33 | 34 | # Ensure the input is lowercase 35 | COLLAPSE=$(printf '%s' "$INPUT_COLLAPSE_OUTPUT" | tr '[:upper:]' '[:lower:]') 36 | if [[ "$COLLAPSE" = true ]]; then 37 | block_md=$(printf '%s\n' \ 38 | '
Terrafetch' \ 39 | '' \ 40 | '```console' \ 41 | "$TERRAFETCH_OUTPUT" \ 42 | '```' \ 43 | '
') 44 | else 45 | block_md=$(printf '%s\n' \ 46 | '```console' \ 47 | "$TERRAFETCH_OUTPUT" \ 48 | '```') 49 | fi 50 | 51 | awk -v block="$block_md" ' 52 | BEGIN {inside = 0} 53 | // {print; print block; inside = 1; next} 54 | // {inside = 0} 55 | !inside {print} 56 | ' "$outfile" >"$tmp" 57 | 58 | mv "$tmp" "$outfile" 59 | 60 | # Commit when file changes 61 | echo "::group::Commit & push (if needed)" 62 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 63 | git config --global user.name "github-actions[bot]" 64 | 65 | if git diff --quiet "$outfile"; then 66 | echo "No changes detected – skipping commit." 67 | else 68 | git add "$outfile" 69 | git commit -m "chore: update Terrafetch section" 70 | git push "https://${INPUT_GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:$(git rev-parse --abbrev-ref HEAD) 71 | fi 72 | echo "::endgroup::" 73 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: terrafetch 2 | description: Add flair to your infrastructure repositories with Terrafetch. 3 | author: RoseSecurity 4 | 5 | branding: 6 | icon: cloud 7 | color: purple 8 | 9 | inputs: 10 | github_token: 11 | description: GITHUB_TOKEN used for committing README updates 12 | default: ${{ github.token }} 13 | required: true 14 | output_file: 15 | description: README-style file that contains markers 16 | default: README.md 17 | collapse_output: 18 | description: Set to **true** to fold Terrafetch output into a `
` block; **false** shows the full log 19 | default: "true" 20 | terraform_directory: 21 | description: Directory holding Terraform code to analyze 22 | default: . 23 | terrafetch_version: 24 | description: Terrafetch version (e.g. 0.2.0 or "latest") 25 | default: latest 26 | 27 | outputs: 28 | terrafetch-return-code: 29 | description: Exit code from the terrafetch run 30 | value: ${{ steps.terrafetch.outputs.terrafetch-return-code }} 31 | 32 | runs: 33 | using: composite 34 | steps: 35 | - id: terrafetch 36 | run: $GITHUB_ACTION_PATH/action.sh 37 | shell: bash 38 | env: 39 | INPUT_GITHUB_TOKEN: ${{ inputs.github_token }} 40 | INPUT_OUTPUT_FILE: ${{ inputs.output_file }} 41 | INPUT_COLLAPSE_OUTPUT: ${{ inputs.collapse_output }} 42 | INPUT_TERRAFORM_DIRECTORY: ${{ inputs.terraform_directory }} 43 | INPUT_TERRAFETCH_VERSION: ${{ inputs.terrafetch_version }} 44 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/RoseSecurity/terrafetch/internal" 7 | "github.com/RoseSecurity/terrafetch/pkg/tui" 8 | log "github.com/charmbracelet/log" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var directory string 13 | 14 | var rootCmd = &cobra.Command{ 15 | Use: "terrafetch", 16 | Short: "The Neofetch of Terraform", 17 | Long: `Turning infrastructure repository statistics into stylish terminal outputs since 2025.`, 18 | RunE: fetchInfo, 19 | SilenceUsage: true, 20 | } 21 | 22 | func init() { 23 | // ‑‑directory / -d flag to point at Terraform code (defaults to current dir) 24 | rootCmd.Flags().StringVarP(&directory, "directory", "d", ".", "Directory containing the Terraform configurations") 25 | rootCmd.DisableAutoGenTag = true // keep generated docs clean 26 | } 27 | 28 | // fetchInfo gathers repository analytics and hands them to the TUI. 29 | // It bubbles any failure up to Cobra so the CLI exits with non‑zero status. 30 | func fetchInfo(cmd *cobra.Command, args []string) error { 31 | analytics, err := internal.AnalyzeRepository(directory) 32 | if err != nil { 33 | log.Error(internal.ErrFailedToFetch, err) 34 | return err 35 | } 36 | 37 | if len(analytics) == 0 { 38 | return internal.ErrNoTerraformFiles 39 | } 40 | 41 | tui.DisplayInfo(directory, analytics[0]) 42 | return nil 43 | } 44 | 45 | func Execute() { 46 | if err := rootCmd.Execute(); err != nil { 47 | os.Exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/img/terrafetch-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoseSecurity/terrafetch/9252644d6b69c2347181d630771e2ab5e9243c83/docs/img/terrafetch-demo.gif -------------------------------------------------------------------------------- /docs/img/terrafetch-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoseSecurity/terrafetch/9252644d6b69c2347181d630771e2ab5e9243c83/docs/img/terrafetch-logo.png -------------------------------------------------------------------------------- /docs/terrafetch-demo.tape: -------------------------------------------------------------------------------- 1 | Output terrafetch-demo.gif 2 | 3 | Set WindowBar Colorful 4 | Set CursorBlink true 5 | Set Margin 20 6 | Set MarginFill "#674EFF" 7 | Set TypingSpeed 75ms 8 | Set Theme "Monokai Vivid" 9 | Set Padding 12 10 | Set PlaybackSpeed 1 11 | Set BorderRadius 10 12 | Set Width 1300 13 | Set Height 650 14 | Set FontSize 14 15 | 16 | Type "terrafetch" 17 | Sleep 1s 18 | Enter 19 | Sleep 4s 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/RoseSecurity/terrafetch 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/charmbracelet/bubbletea v1.3.10 7 | github.com/charmbracelet/lipgloss v1.1.0 8 | github.com/charmbracelet/log v0.4.2 9 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250828155816-225c06ed5fd9 10 | github.com/mattn/go-runewidth v0.0.19 11 | github.com/spf13/cobra v1.10.1 12 | ) 13 | 14 | require ( 15 | github.com/agext/levenshtein v1.2.2 // indirect 16 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 17 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 18 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 19 | github.com/charmbracelet/x/ansi v0.10.1 // indirect 20 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 21 | github.com/charmbracelet/x/term v0.2.1 // indirect 22 | github.com/clipperhouse/uax29/v2 v2.2.0 // indirect 23 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 24 | github.com/go-logfmt/logfmt v0.6.0 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f // indirect 27 | github.com/hashicorp/hcl/v2 v2.20.1 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/mattn/go-localereader v0.0.1 // indirect 32 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 33 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 34 | github.com/muesli/cancelreader v0.2.2 // indirect 35 | github.com/muesli/termenv v0.16.0 // indirect 36 | github.com/rivo/uniseg v0.4.7 // indirect 37 | github.com/spf13/pflag v1.0.9 // indirect 38 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 39 | github.com/zclconf/go-cty v1.14.4 // indirect 40 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 41 | golang.org/x/mod v0.13.0 // indirect 42 | golang.org/x/sync v0.15.0 // indirect 43 | golang.org/x/sys v0.36.0 // indirect 44 | golang.org/x/text v0.11.0 // indirect 45 | golang.org/x/tools v0.14.0 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= 2 | github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 3 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 4 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 5 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 | github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= 8 | github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= 9 | github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= 10 | github.com/charmbracelet/bubbletea v1.3.8/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 11 | github.com/charmbracelet/bubbletea v1.3.9/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 12 | github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 13 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 14 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 15 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 16 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 17 | github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= 18 | github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= 19 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 20 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 21 | github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= 22 | github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 23 | github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= 24 | github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 25 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 26 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 27 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 28 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 29 | github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= 30 | github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 31 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 32 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 35 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 36 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 37 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 38 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 39 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 40 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 41 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 42 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= 43 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 44 | github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= 45 | github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= 46 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250515145901-f4c50e64fd6d h1:OtLDHysmVW+zEeWF7VWRvGALkOgws3cmDd2QI1xAAhw= 47 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250515145901-f4c50e64fd6d/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= 48 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250731202709-e8a84eebd3e7 h1:NIwoxkIKF4OAbTze/gukXLJqS9MISRXCqn4SnurhZ1I= 49 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250731202709-e8a84eebd3e7/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= 50 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250815164439-e06743db9cd8 h1:zaEwEjVlZ9ddULw1WjhuPuqWAyUmfbIsSyVC1TUImCM= 51 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250815164439-e06743db9cd8/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= 52 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250822072239-d9a8f4395430 h1:0m8+HFw2xV7ga/CWq5ZHsMoBm6rrJKTkNATNm9HaarQ= 53 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250822072239-d9a8f4395430/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= 54 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250828155816-225c06ed5fd9 h1:1MLunZqjVd9j5gc5kjE04VEoieDVdWdgdM6T2fNQvY8= 55 | github.com/hashicorp/terraform-config-inspect v0.0.0-20250828155816-225c06ed5fd9/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= 56 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 57 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 58 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 59 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 60 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 61 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 62 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 63 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 64 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 65 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 66 | github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= 67 | github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 68 | github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= 69 | github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 70 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 71 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 72 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 73 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 74 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 75 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 76 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 77 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 81 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 82 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 83 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 84 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 85 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 86 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 87 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 88 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 89 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 90 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 91 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 92 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 93 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 94 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 95 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 96 | github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= 97 | github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 98 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= 99 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= 100 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 101 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 102 | golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= 103 | golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 104 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 105 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 106 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 107 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 108 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 110 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 111 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 112 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 113 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 114 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 115 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 116 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 117 | golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= 118 | golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 119 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 120 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 121 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 122 | -------------------------------------------------------------------------------- /internal/analyze.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/RoseSecurity/terrafetch/pkg/utils" 5 | log "github.com/charmbracelet/log" 6 | "github.com/hashicorp/terraform-config-inspect/tfconfig" 7 | ) 8 | 9 | type Analytics struct { 10 | VariableCount int 11 | SensitiveVariableCount int 12 | ResourceCount int 13 | OutputCount int 14 | SensitiveOutputCount int 15 | DataSourceCount int 16 | ProviderCount int 17 | ModuleCount int 18 | FileCount int 19 | DocCount int 20 | } 21 | 22 | func AnalyzeRepository(rootDir string) ([]Analytics, error) { 23 | dirs, err := utils.FindTFDirs(rootDir) 24 | if err != nil { 25 | return nil, ErrFailedToFindDir 26 | } 27 | 28 | if len(dirs) == 0 { 29 | return nil, ErrNoTerraformFiles 30 | } 31 | 32 | var totalVars, totalResources, totalOutputs, totalDataSources, totalModules, totalProviders, totalSensitiveVars, totalSensitiveOutputs int 33 | 34 | for dir := range dirs { 35 | if !isTerraformDirectory(dir) { 36 | continue 37 | } 38 | 39 | repo, diags := tfconfig.LoadModule(dir) 40 | if diags.HasErrors() { 41 | log.Warn("could not load %v", dir) 42 | } 43 | 44 | totalVars += len(repo.Variables) 45 | totalOutputs += len(repo.Outputs) 46 | totalResources += len(repo.ManagedResources) 47 | totalDataSources += len(repo.DataResources) 48 | totalModules += len(repo.ModuleCalls) 49 | totalProviders += len(repo.RequiredProviders) 50 | 51 | for _, v := range repo.Variables { 52 | if v.Sensitive { 53 | totalSensitiveVars++ 54 | } 55 | } 56 | 57 | for _, v := range repo.Outputs { 58 | if v.Sensitive { 59 | totalSensitiveOutputs++ 60 | } 61 | } 62 | } 63 | 64 | totalTfFiles, totalDocFiles, err := utils.FindFiles(rootDir) 65 | if err != nil { 66 | log.Error("could not count terraform files %v", err) 67 | } 68 | 69 | return []Analytics{ 70 | { 71 | VariableCount: totalVars, 72 | SensitiveVariableCount: totalSensitiveVars, 73 | ResourceCount: totalResources, 74 | OutputCount: totalOutputs, 75 | SensitiveOutputCount: totalSensitiveOutputs, 76 | DataSourceCount: totalDataSources, 77 | ProviderCount: totalProviders, 78 | ModuleCount: totalModules, 79 | FileCount: totalTfFiles, 80 | DocCount: totalDocFiles, 81 | }, 82 | }, nil 83 | } 84 | 85 | // isTerraformDirectory returns if a directory contains Terraform code 86 | func isTerraformDirectory(dir string) bool { 87 | return tfconfig.IsModuleDir(dir) 88 | } 89 | -------------------------------------------------------------------------------- /internal/error.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNoTerraformFiles = errors.New("the provided directory does not contain terraform code") 7 | ErrFailedToFetch = errors.New("failed to fetch repository analytics") 8 | ErrFailedToFindDir = errors.New("failed to find terraform directory") 9 | ErrFailedToFindCode = errors.New("failed to find terraform code") 10 | ) 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/RoseSecurity/terrafetch/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /pkg/tui/logo.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | const logo = ` 4 | @# 5 | @@@@@ 6 | @@@@@@@@. 7 | @@@@@@@@@@ + # 8 | @@@@@@@@@@ @@@@ @@@@ 9 | @@@@@@@@@@ @@@@@@@. .@@@@@@@ 10 | @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ 11 | +@@@@@ @@@@@@@@@@ @@@@@@@@@@ 12 | .@@ @@@@@@@@@@ @@@@@@@@@@ 13 | @@@@@@@@@@ @@@@@@@@@@ 14 | @+ -@@@@@@ @@@@@@= 15 | @@@@@ .@@@ @@@. 16 | @@@@@@@@. 17 | @@@@@@@@@@ 18 | @@@@@@@@@@ 19 | @@@@@@@@@@ 20 | .@@@@@@@@ 21 | @@@@@ 22 | %@ 23 | ` 24 | -------------------------------------------------------------------------------- /pkg/tui/ui.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/RoseSecurity/terrafetch/internal" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/mattn/go-runewidth" 11 | ) 12 | 13 | var ( 14 | borderColor = lipgloss.Color("99") // purple 15 | container = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(borderColor) 16 | 17 | headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")) // magenta header 18 | keyBase = lipgloss.NewStyle().Foreground(lipgloss.Color("111")) // cyan keys (base style) 19 | valStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15")) // white values 20 | logoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99")) // purple logo 21 | ) 22 | 23 | // padKey dynamically pads and styles a key string based on the provided style 24 | // (whose width is already set to the widest key + 1 for the trailing colon). 25 | func padKey(k string, style lipgloss.Style) string { 26 | return style.Render(k + ":") 27 | } 28 | 29 | func RenderInfo(dir string, a internal.Analytics) string { 30 | // All label strings in the order they'll be rendered. 31 | labels := []string{ 32 | "Terraform Files", 33 | "Documentation", 34 | "Providers", 35 | "Module Calls", 36 | "Resources", 37 | "Data Sources", 38 | "Variables", 39 | "Sensitive Variables", 40 | "Outputs", 41 | "Sensitive Outputs", 42 | } 43 | 44 | // Find the widest label using runewidth (handles double‑width runes correctly). 45 | max := 0 46 | for _, l := range labels { 47 | if w := runewidth.StringWidth(l); w > max { 48 | max = w 49 | } 50 | } 51 | 52 | // Clone the base key style and set its width dynamically. 53 | keyStyle := keyBase.Width(max + 1) // +1 for the trailing colon 54 | 55 | tfDir := filepath.Base(dir) 56 | 57 | // Helper closure so we don't repeat ourselves. 58 | pk := func(k string) string { return padKey(k, keyStyle) } 59 | 60 | lines := []string{ 61 | headerStyle.Render(tfDir), 62 | headerStyle.Render(strings.Repeat("-", runewidth.StringWidth(tfDir))), 63 | fmt.Sprintf("%s %s", pk("Terraform Files"), valStyle.Render(fmt.Sprint(a.FileCount))), 64 | fmt.Sprintf("%s %s", pk("Documentation"), valStyle.Render(fmt.Sprint(a.DocCount))), 65 | fmt.Sprintf("%s %s", pk("Providers"), valStyle.Render(fmt.Sprint(a.ProviderCount))), 66 | fmt.Sprintf("%s %s", pk("Module Calls"), valStyle.Render(fmt.Sprint(a.ModuleCount))), 67 | fmt.Sprintf("%s %s", pk("Resources"), valStyle.Render(fmt.Sprint(a.ResourceCount))), 68 | fmt.Sprintf("%s %s", pk("Data Sources"), valStyle.Render(fmt.Sprint(a.DataSourceCount))), 69 | fmt.Sprintf("%s %s", pk("Variables"), valStyle.Render(fmt.Sprint(a.VariableCount))), 70 | fmt.Sprintf("%s %s", pk("Sensitive Variables"), valStyle.Render(fmt.Sprint(a.SensitiveVariableCount))), 71 | fmt.Sprintf("%s %s", pk("Outputs"), valStyle.Render(fmt.Sprint(a.OutputCount))), 72 | fmt.Sprintf("%s %s", pk("Sensitive Outputs"), valStyle.Render(fmt.Sprint(a.SensitiveOutputCount))), 73 | } 74 | 75 | rightColumn := lipgloss.NewStyle(). 76 | PaddingLeft(4). 77 | PaddingRight(4). 78 | Render(strings.Join(lines, "\n")) 79 | 80 | logoColumn := logoStyle.Render(logo) 81 | 82 | return container.Render( 83 | lipgloss.JoinHorizontal(lipgloss.Top, logoColumn, rightColumn), 84 | ) 85 | } 86 | 87 | func DisplayInfo(dir string, a internal.Analytics) { 88 | fmt.Println(RenderInfo(dir, a)) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/utils/dirs.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/fs" 5 | "path/filepath" 6 | "strings" 7 | 8 | log "github.com/charmbracelet/log" 9 | ) 10 | 11 | // FindTFDirs returns an array of directories containing Terraform code 12 | func FindTFDirs(dir string) (map[string]struct{}, error) { 13 | tfDirs := make(map[string]struct{}) 14 | 15 | err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 16 | if err != nil { 17 | log.Error("error accessing path %q: %v", path, err) 18 | return nil 19 | } 20 | 21 | if d.IsDir() && d.Name() == ".terraform" { 22 | return filepath.SkipDir 23 | } 24 | 25 | if !d.IsDir() && filepath.Ext(d.Name()) == ".tf" { 26 | parent := filepath.Dir(path) 27 | tfDirs[parent] = struct{}{} 28 | } 29 | 30 | return nil 31 | }) 32 | if err != nil { 33 | log.Error("error walking the path %q: %v", dir, err) 34 | return nil, err 35 | } 36 | 37 | return tfDirs, nil 38 | } 39 | 40 | func FindFiles(root string) (int, int, error) { 41 | var tfCount, docCount int 42 | 43 | skipDirs := map[string]bool{ 44 | ".terraform": true, 45 | ".git": true, 46 | "vendor": true, 47 | "test": true, 48 | } 49 | 50 | err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // Skip ignored directories 56 | if d.IsDir() && skipDirs[d.Name()] { 57 | return fs.SkipDir 58 | } 59 | 60 | name := d.Name() 61 | 62 | if !d.IsDir() && (strings.HasSuffix(name, ".tf") || strings.HasSuffix(name, ".tofu")) { 63 | tfCount++ 64 | } 65 | 66 | // Match documentation files 67 | if !d.IsDir() && 68 | (strings.HasPrefix(strings.ToLower(name), "readme") || 69 | strings.HasPrefix(strings.ToLower(name), "contributing") || 70 | strings.Contains(path, string(filepath.Separator)+"docs"+string(filepath.Separator)) || 71 | strings.Contains(path, string(filepath.Separator)+"examples"+string(filepath.Separator))) { 72 | docCount++ 73 | } 74 | 75 | return nil 76 | }) 77 | 78 | return tfCount, docCount, err 79 | } 80 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended" 4 | ], 5 | "packageRules": [ 6 | { 7 | "description": "Update Go Dependencies", 8 | "matchManagers": [ 9 | "gomod" 10 | ], 11 | "schedule": [ 12 | "at any time" 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/account-map/README.md: -------------------------------------------------------------------------------- 1 | # `account-map` 2 | 3 | This component is not the true `account-map` component. This repo is used for demo purposes and therefore is drastically 4 | simplified. 5 | -------------------------------------------------------------------------------- /tests/account-map/modules/iam-roles/README.md: -------------------------------------------------------------------------------- 1 | # Submodule `iam-roles` 2 | 3 | This is not the true `iam-roles` submodule. This implementation is drastically simplified for demos 4 | -------------------------------------------------------------------------------- /tests/account-map/modules/iam-roles/context.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 3 | # All other instances of this file should be a copy of that one 4 | # 5 | # 6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 7 | # and then place it in your Terraform module to automatically get 8 | # Cloud Posse's standard configuration inputs suitable for passing 9 | # to Cloud Posse modules. 10 | # 11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf 12 | # 13 | # Modules should access the whole context as `module.this.context` 14 | # to get the input variables with nulls for defaults, 15 | # for example `context = module.this.context`, 16 | # and access individual variables as `module.this.`, 17 | # with final values filled in. 18 | # 19 | # For example, when using defaults, `module.this.context.delimiter` 20 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 21 | # 22 | 23 | module "this" { 24 | source = "cloudposse/label/null" 25 | version = "0.25.0" # requires Terraform >= 0.13.0 26 | 27 | enabled = var.enabled 28 | namespace = var.namespace 29 | tenant = var.tenant 30 | environment = var.environment 31 | stage = var.stage 32 | name = var.name 33 | delimiter = var.delimiter 34 | attributes = var.attributes 35 | tags = var.tags 36 | additional_tag_map = var.additional_tag_map 37 | label_order = var.label_order 38 | regex_replace_chars = var.regex_replace_chars 39 | id_length_limit = var.id_length_limit 40 | label_key_case = var.label_key_case 41 | label_value_case = var.label_value_case 42 | descriptor_formats = var.descriptor_formats 43 | labels_as_tags = var.labels_as_tags 44 | 45 | context = var.context 46 | } 47 | 48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 49 | 50 | variable "context" { 51 | type = any 52 | default = { 53 | enabled = true 54 | namespace = null 55 | tenant = null 56 | environment = null 57 | stage = null 58 | name = null 59 | delimiter = null 60 | attributes = [] 61 | tags = {} 62 | additional_tag_map = {} 63 | regex_replace_chars = null 64 | label_order = [] 65 | id_length_limit = null 66 | label_key_case = null 67 | label_value_case = null 68 | descriptor_formats = {} 69 | # Note: we have to use [] instead of null for unset lists due to 70 | # https://github.com/hashicorp/terraform/issues/28137 71 | # which was not fixed until Terraform 1.0.0, 72 | # but we want the default to be all the labels in `label_order` 73 | # and we want users to be able to prevent all tag generation 74 | # by setting `labels_as_tags` to `[]`, so we need 75 | # a different sentinel to indicate "default" 76 | labels_as_tags = ["unset"] 77 | } 78 | description = <<-EOT 79 | Single object for setting entire context at once. 80 | See description of individual variables for details. 81 | Leave string and numeric variables as `null` to use default value. 82 | Individual variable settings (non-null) override settings in context object, 83 | except for attributes, tags, and additional_tag_map, which are merged. 84 | EOT 85 | 86 | validation { 87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 88 | error_message = "Allowed values: `lower`, `title`, `upper`." 89 | } 90 | 91 | validation { 92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 94 | } 95 | } 96 | 97 | variable "enabled" { 98 | type = bool 99 | default = null 100 | description = "Set to false to prevent the module from creating any resources" 101 | } 102 | 103 | variable "namespace" { 104 | type = string 105 | default = null 106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 107 | } 108 | 109 | variable "tenant" { 110 | type = string 111 | default = null 112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 113 | } 114 | 115 | variable "environment" { 116 | type = string 117 | default = null 118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 119 | } 120 | 121 | variable "stage" { 122 | type = string 123 | default = null 124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 125 | } 126 | 127 | variable "name" { 128 | type = string 129 | default = null 130 | description = <<-EOT 131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 132 | This is the only ID element not also included as a `tag`. 133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 134 | EOT 135 | } 136 | 137 | variable "delimiter" { 138 | type = string 139 | default = null 140 | description = <<-EOT 141 | Delimiter to be used between ID elements. 142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 143 | EOT 144 | } 145 | 146 | variable "attributes" { 147 | type = list(string) 148 | default = [] 149 | description = <<-EOT 150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 151 | in the order they appear in the list. New attributes are appended to the 152 | end of the list. The elements of the list are joined by the `delimiter` 153 | and treated as a single ID element. 154 | EOT 155 | } 156 | 157 | variable "labels_as_tags" { 158 | type = set(string) 159 | default = ["default"] 160 | description = <<-EOT 161 | Set of labels (ID elements) to include as tags in the `tags` output. 162 | Default is to include all labels. 163 | Tags with empty values will not be included in the `tags` output. 164 | Set to `[]` to suppress all generated tags. 165 | **Notes:** 166 | The value of the `name` tag, if included, will be the `id`, not the `name`. 167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 168 | changed in later chained modules. Attempts to change it will be silently ignored. 169 | EOT 170 | } 171 | 172 | variable "tags" { 173 | type = map(string) 174 | default = {} 175 | description = <<-EOT 176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 177 | Neither the tag keys nor the tag values will be modified by this module. 178 | EOT 179 | } 180 | 181 | variable "additional_tag_map" { 182 | type = map(string) 183 | default = {} 184 | description = <<-EOT 185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 186 | This is for some rare cases where resources want additional configuration of tags 187 | and therefore take a list of maps with tag key, value, and additional configuration. 188 | EOT 189 | } 190 | 191 | variable "label_order" { 192 | type = list(string) 193 | default = null 194 | description = <<-EOT 195 | The order in which the labels (ID elements) appear in the `id`. 196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 198 | EOT 199 | } 200 | 201 | variable "regex_replace_chars" { 202 | type = string 203 | default = null 204 | description = <<-EOT 205 | Terraform regular expression (regex) string. 206 | Characters matching the regex will be removed from the ID elements. 207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 208 | EOT 209 | } 210 | 211 | variable "id_length_limit" { 212 | type = number 213 | default = null 214 | description = <<-EOT 215 | Limit `id` to this many characters (minimum 6). 216 | Set to `0` for unlimited length. 217 | Set to `null` for keep the existing setting, which defaults to `0`. 218 | Does not affect `id_full`. 219 | EOT 220 | validation { 221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 223 | } 224 | } 225 | 226 | variable "label_key_case" { 227 | type = string 228 | default = null 229 | description = <<-EOT 230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 231 | Does not affect keys of tags passed in via the `tags` input. 232 | Possible values: `lower`, `title`, `upper`. 233 | Default value: `title`. 234 | EOT 235 | 236 | validation { 237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 238 | error_message = "Allowed values: `lower`, `title`, `upper`." 239 | } 240 | } 241 | 242 | variable "label_value_case" { 243 | type = string 244 | default = null 245 | description = <<-EOT 246 | Controls the letter case of ID elements (labels) as included in `id`, 247 | set as tag values, and output by this module individually. 248 | Does not affect values of tags passed in via the `tags` input. 249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 251 | Default value: `lower`. 252 | EOT 253 | 254 | validation { 255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 257 | } 258 | } 259 | 260 | variable "descriptor_formats" { 261 | type = any 262 | default = {} 263 | description = <<-EOT 264 | Describe additional descriptors to be output in the `descriptors` output map. 265 | Map of maps. Keys are names of descriptors. Values are maps of the form 266 | `{ 267 | format = string 268 | labels = list(string) 269 | }` 270 | (Type is `any` so the map values can later be enhanced to provide additional options.) 271 | `format` is a Terraform format string to be passed to the `format()` function. 272 | `labels` is a list of labels, in order, to pass to `format()` function. 273 | Label values will be normalized before being passed to `format()` so they will be 274 | identical to how they appear in `id`. 275 | Default is `{}` (`descriptors` output will be empty). 276 | EOT 277 | } 278 | 279 | #### End of copy of cloudposse/terraform-null-label/variables.tf 280 | -------------------------------------------------------------------------------- /tests/account-map/modules/iam-roles/main.tf: -------------------------------------------------------------------------------- 1 | # THIS IS NOT THE TRUE IMPLEMENTATION! 2 | # We have drastically simplified the module here for demo sake 3 | locals { 4 | terraform_role_map = { 5 | "core-auto" = "arn:aws:iam::461333128641:role/cptest-core-gbl-auto-terraform" # this is the true core-auto, where gitops resources are deployed 6 | "plat-dev" = "arn:aws:iam::630114703016:role/cptest-plat-gbl-sandbox-terraform" # this is actually plat-sandbox. For testing we are intentionally deploying everything into sandbox 7 | "plat-staging" = "arn:aws:iam::630114703016:role/cptest-plat-gbl-sandbox-terraform" # ^ 8 | "plat-prod" = "arn:aws:iam::630114703016:role/cptest-plat-gbl-sandbox-terraform" # ^^ 9 | } 10 | 11 | account_id = "${module.this.tenant}-${module.this.stage}" 12 | terraform_role_arn = local.terraform_role_map[local.account_id] 13 | terraform_profile_name = null 14 | } 15 | -------------------------------------------------------------------------------- /tests/account-map/modules/iam-roles/outputs.tf: -------------------------------------------------------------------------------- 1 | output "terraform_role_arn" { 2 | value = local.terraform_role_arn 3 | description = "The AWS Role ARN for Terraform to use when provisioning resources in the account, when Role ARNs are in use" 4 | } 5 | 6 | output "terraform_profile_name" { 7 | value = local.terraform_profile_name 8 | description = "The AWS config profile name for Terraform to use when provisioning resources in the account, when profiles are in use" 9 | } 10 | -------------------------------------------------------------------------------- /tests/account-map/modules/iam-roles/providers.tf: -------------------------------------------------------------------------------- 1 | provider "awsutils" { 2 | # Components may want to use awsutils, and when they do, they typically want to use it in the assumed IAM role. 3 | # That conflicts with this module's needs, so we create a separate provider alias for this module to use. 4 | alias = "iam-roles" 5 | 6 | # If the provider block is empty, Terraform will output a deprecation warning, 7 | # because earlier versions of Terraform used empty provider blocks to declare provider requirements, 8 | # which is now deprecated in favor of the required_providers block. 9 | # So we add a useless setting to the provider block to avoid the deprecation warning. 10 | profile = null 11 | } 12 | -------------------------------------------------------------------------------- /tests/account-map/modules/iam-roles/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.0" 3 | 4 | required_providers { 5 | awsutils = { 6 | source = "cloudposse/awsutils" 7 | version = ">= 0.16.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/account-map/modules/team-assume-role-policy/README.md: -------------------------------------------------------------------------------- 1 | # Submodule `team-assume-role-policy` 2 | 3 | This is not the true `team-assume-role-policy` submodule. This implementation is drastically simplified for demos 4 | -------------------------------------------------------------------------------- /tests/account-map/modules/team-assume-role-policy/context.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 3 | # All other instances of this file should be a copy of that one 4 | # 5 | # 6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 7 | # and then place it in your Terraform module to automatically get 8 | # Cloud Posse's standard configuration inputs suitable for passing 9 | # to Cloud Posse modules. 10 | # 11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf 12 | # 13 | # Modules should access the whole context as `module.this.context` 14 | # to get the input variables with nulls for defaults, 15 | # for example `context = module.this.context`, 16 | # and access individual variables as `module.this.`, 17 | # with final values filled in. 18 | # 19 | # For example, when using defaults, `module.this.context.delimiter` 20 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 21 | # 22 | 23 | module "this" { 24 | source = "cloudposse/label/null" 25 | version = "0.25.0" # requires Terraform >= 0.13.0 26 | 27 | enabled = var.enabled 28 | namespace = var.namespace 29 | tenant = var.tenant 30 | environment = var.environment 31 | stage = var.stage 32 | name = var.name 33 | delimiter = var.delimiter 34 | attributes = var.attributes 35 | tags = var.tags 36 | additional_tag_map = var.additional_tag_map 37 | label_order = var.label_order 38 | regex_replace_chars = var.regex_replace_chars 39 | id_length_limit = var.id_length_limit 40 | label_key_case = var.label_key_case 41 | label_value_case = var.label_value_case 42 | descriptor_formats = var.descriptor_formats 43 | labels_as_tags = var.labels_as_tags 44 | 45 | context = var.context 46 | } 47 | 48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 49 | 50 | variable "context" { 51 | type = any 52 | default = { 53 | enabled = true 54 | namespace = null 55 | tenant = null 56 | environment = null 57 | stage = null 58 | name = null 59 | delimiter = null 60 | attributes = [] 61 | tags = {} 62 | additional_tag_map = {} 63 | regex_replace_chars = null 64 | label_order = [] 65 | id_length_limit = null 66 | label_key_case = null 67 | label_value_case = null 68 | descriptor_formats = {} 69 | # Note: we have to use [] instead of null for unset lists due to 70 | # https://github.com/hashicorp/terraform/issues/28137 71 | # which was not fixed until Terraform 1.0.0, 72 | # but we want the default to be all the labels in `label_order` 73 | # and we want users to be able to prevent all tag generation 74 | # by setting `labels_as_tags` to `[]`, so we need 75 | # a different sentinel to indicate "default" 76 | labels_as_tags = ["unset"] 77 | } 78 | description = <<-EOT 79 | Single object for setting entire context at once. 80 | See description of individual variables for details. 81 | Leave string and numeric variables as `null` to use default value. 82 | Individual variable settings (non-null) override settings in context object, 83 | except for attributes, tags, and additional_tag_map, which are merged. 84 | EOT 85 | 86 | validation { 87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 88 | error_message = "Allowed values: `lower`, `title`, `upper`." 89 | } 90 | 91 | validation { 92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 94 | } 95 | } 96 | 97 | variable "enabled" { 98 | type = bool 99 | default = null 100 | description = "Set to false to prevent the module from creating any resources" 101 | } 102 | 103 | variable "namespace" { 104 | type = string 105 | default = null 106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 107 | } 108 | 109 | variable "tenant" { 110 | type = string 111 | default = null 112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 113 | } 114 | 115 | variable "environment" { 116 | type = string 117 | default = null 118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 119 | } 120 | 121 | variable "stage" { 122 | type = string 123 | default = null 124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 125 | } 126 | 127 | variable "name" { 128 | type = string 129 | default = null 130 | description = <<-EOT 131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 132 | This is the only ID element not also included as a `tag`. 133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 134 | EOT 135 | } 136 | 137 | variable "delimiter" { 138 | type = string 139 | default = null 140 | description = <<-EOT 141 | Delimiter to be used between ID elements. 142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 143 | EOT 144 | } 145 | 146 | variable "attributes" { 147 | type = list(string) 148 | default = [] 149 | description = <<-EOT 150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 151 | in the order they appear in the list. New attributes are appended to the 152 | end of the list. The elements of the list are joined by the `delimiter` 153 | and treated as a single ID element. 154 | EOT 155 | } 156 | 157 | variable "labels_as_tags" { 158 | type = set(string) 159 | default = ["default"] 160 | description = <<-EOT 161 | Set of labels (ID elements) to include as tags in the `tags` output. 162 | Default is to include all labels. 163 | Tags with empty values will not be included in the `tags` output. 164 | Set to `[]` to suppress all generated tags. 165 | **Notes:** 166 | The value of the `name` tag, if included, will be the `id`, not the `name`. 167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 168 | changed in later chained modules. Attempts to change it will be silently ignored. 169 | EOT 170 | } 171 | 172 | variable "tags" { 173 | type = map(string) 174 | default = {} 175 | description = <<-EOT 176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 177 | Neither the tag keys nor the tag values will be modified by this module. 178 | EOT 179 | } 180 | 181 | variable "additional_tag_map" { 182 | type = map(string) 183 | default = {} 184 | description = <<-EOT 185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 186 | This is for some rare cases where resources want additional configuration of tags 187 | and therefore take a list of maps with tag key, value, and additional configuration. 188 | EOT 189 | } 190 | 191 | variable "label_order" { 192 | type = list(string) 193 | default = null 194 | description = <<-EOT 195 | The order in which the labels (ID elements) appear in the `id`. 196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 198 | EOT 199 | } 200 | 201 | variable "regex_replace_chars" { 202 | type = string 203 | default = null 204 | description = <<-EOT 205 | Terraform regular expression (regex) string. 206 | Characters matching the regex will be removed from the ID elements. 207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 208 | EOT 209 | } 210 | 211 | variable "id_length_limit" { 212 | type = number 213 | default = null 214 | description = <<-EOT 215 | Limit `id` to this many characters (minimum 6). 216 | Set to `0` for unlimited length. 217 | Set to `null` for keep the existing setting, which defaults to `0`. 218 | Does not affect `id_full`. 219 | EOT 220 | validation { 221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 223 | } 224 | } 225 | 226 | variable "label_key_case" { 227 | type = string 228 | default = null 229 | description = <<-EOT 230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 231 | Does not affect keys of tags passed in via the `tags` input. 232 | Possible values: `lower`, `title`, `upper`. 233 | Default value: `title`. 234 | EOT 235 | 236 | validation { 237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 238 | error_message = "Allowed values: `lower`, `title`, `upper`." 239 | } 240 | } 241 | 242 | variable "label_value_case" { 243 | type = string 244 | default = null 245 | description = <<-EOT 246 | Controls the letter case of ID elements (labels) as included in `id`, 247 | set as tag values, and output by this module individually. 248 | Does not affect values of tags passed in via the `tags` input. 249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 251 | Default value: `lower`. 252 | EOT 253 | 254 | validation { 255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 257 | } 258 | } 259 | 260 | variable "descriptor_formats" { 261 | type = any 262 | default = {} 263 | description = <<-EOT 264 | Describe additional descriptors to be output in the `descriptors` output map. 265 | Map of maps. Keys are names of descriptors. Values are maps of the form 266 | `{ 267 | format = string 268 | labels = list(string) 269 | }` 270 | (Type is `any` so the map values can later be enhanced to provide additional options.) 271 | `format` is a Terraform format string to be passed to the `format()` function. 272 | `labels` is a list of labels, in order, to pass to `format()` function. 273 | Label values will be normalized before being passed to `format()` so they will be 274 | identical to how they appear in `id`. 275 | Default is `{}` (`descriptors` output will be empty). 276 | EOT 277 | } 278 | 279 | #### End of copy of cloudposse/terraform-null-label/variables.tf 280 | -------------------------------------------------------------------------------- /tests/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf: -------------------------------------------------------------------------------- 1 | variable "trusted_github_repos" { 2 | type = list(string) 3 | description = <<-EOT 4 | A list of GitHub repositories allowed to access this role. 5 | Format is either "orgName/repoName" or just "repoName", 6 | in which case "cloudposse" will be used for the "orgName". 7 | Wildcard ("*") is allowed for "repoName". 8 | EOT 9 | default = [] 10 | } 11 | 12 | variable "trusted_github_org" { 13 | type = string 14 | description = "The GitHub organization unqualified repos are assumed to belong to. Keeps `*` from meaning all orgs and all repos." 15 | default = "cloudposse" 16 | } 17 | 18 | variable "global_environment_name" { 19 | type = string 20 | description = "Global environment name" 21 | default = "gbl" 22 | } 23 | 24 | locals { 25 | github_oidc_enabled = length(var.trusted_github_repos) > 0 26 | } 27 | 28 | data "aws_iam_policy_document" "github_oidc_provider_assume" { 29 | count = local.github_oidc_enabled ? 1 : 0 30 | 31 | statement { 32 | sid = "OidcProviderAssume" 33 | actions = [ 34 | "sts:AssumeRoleWithWebIdentity", 35 | "sts:TagSession", 36 | ] 37 | 38 | principals { 39 | type = "Federated" 40 | 41 | identifiers = [one(module.github_oidc_provider[*].outputs.oidc_provider_arn)] 42 | } 43 | 44 | condition { 45 | test = "StringEquals" 46 | variable = "token.actions.githubusercontent.com:aud" 47 | values = ["sts.amazonaws.com"] 48 | } 49 | 50 | condition { 51 | test = "StringLike" 52 | variable = "token.actions.githubusercontent.com:sub" 53 | 54 | values = [for r in var.trusted_github_repos : "repo:${contains(split("", r), "/") ? r : "${var.trusted_github_org}/${r}"}:*"] 55 | } 56 | } 57 | } 58 | 59 | module "github_oidc_provider" { 60 | count = local.github_oidc_enabled ? 1 : 0 61 | 62 | source = "cloudposse/stack-config/yaml//modules/remote-state" 63 | version = "1.5.0" 64 | 65 | component = "github-oidc-provider" 66 | environment = var.global_environment_name 67 | 68 | privileged = var.privileged 69 | 70 | ignore_errors = true 71 | 72 | defaults = { 73 | oidc_provider_arn = "" 74 | } 75 | 76 | context = module.this.context 77 | } 78 | 79 | output "github_assume_role_policy" { 80 | value = one(data.aws_iam_policy_document.github_oidc_provider_assume[*].json) 81 | description = "JSON encoded string representing the \"Assume Role\" policy configured by the inputs" 82 | } 83 | -------------------------------------------------------------------------------- /tests/account-map/modules/team-assume-role-policy/main.tf: -------------------------------------------------------------------------------- 1 | # THIS IS NOT THE TRUE IMPLEMENTATION! 2 | # We have drastically simplified the module here for demo sake 3 | 4 | variable "privileged" { 5 | type = bool 6 | description = "True if the default provider already has access to the backend" 7 | default = false 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/account-map/modules/team-assume-role-policy/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 4.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/account-map/outputs.tf: -------------------------------------------------------------------------------- 1 | output "full_account_map" { 2 | value = { 3 | "core-auto" = "461333128641" # this is the true core-auto, where gitops resources are deployed 4 | "plat-dev" = "630114703016" # this is actually plat-sandbox. For testing we are intentionally deploying everything into sandbox 5 | "plat-staging" = "630114703016" # ^ 6 | "plat-prod" = "630114703016" # ^^ 7 | } 8 | description = "The map of account name to account ID (number)." 9 | } 10 | -------------------------------------------------------------------------------- /tests/account-map/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.0" 3 | } 4 | -------------------------------------------------------------------------------- /tests/api/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the API" 3 | type = string 4 | default = "api" 5 | } 6 | 7 | variable "database_id" { 8 | description = "Mock database ID" 9 | type = string 10 | default = "" 11 | } 12 | 13 | variable "cluster_id" { 14 | description = "Mock cluster ID" 15 | type = string 16 | default = "" 17 | } 18 | 19 | resource "random_id" "id" { 20 | byte_length = 8 21 | } 22 | 23 | locals { 24 | mock_api_id = "${var.name}-${random_id.id.hex}" 25 | } 26 | 27 | output "api_id" { 28 | value = local.mock_api_id 29 | } 30 | 31 | output "database_id" { 32 | value = var.database_id 33 | } 34 | 35 | output "cluster_id" { 36 | value = var.cluster_id 37 | } 38 | -------------------------------------------------------------------------------- /tests/api/providers.tf: -------------------------------------------------------------------------------- 1 | provider "random" { 2 | # Configuration options for the provider (if any) 3 | } 4 | -------------------------------------------------------------------------------- /tests/cache/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the cache" 3 | type = string 4 | default = "cache" 5 | } 6 | 7 | resource "random_id" "id" { 8 | byte_length = 8 9 | } 10 | 11 | locals { 12 | mock_cache_id = "${var.name}-${random_id.id.hex}" 13 | } 14 | 15 | output "cache_id" { 16 | value = local.mock_cache_id 17 | } 18 | -------------------------------------------------------------------------------- /tests/cache/providers.tf: -------------------------------------------------------------------------------- 1 | provider "random" { 2 | # Configuration options for the provider (if any) 3 | } 4 | -------------------------------------------------------------------------------- /tests/cdn/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the CDN" 3 | type = string 4 | default = "cdn" 5 | } 6 | 7 | variable "storage_id" { 8 | description = "Mock input for storage id" 9 | type = string 10 | default = "" 11 | } 12 | 13 | variable "frontend_id" { 14 | description = "Mock input for frontend id" 15 | type = string 16 | default = "" 17 | } 18 | 19 | resource "random_id" "id" { 20 | byte_length = 8 21 | } 22 | 23 | locals { 24 | mock_cdn_id = "${var.name}-${random_id.id.hex}" 25 | } 26 | 27 | output "cdn_id" { 28 | value = local.mock_cdn_id 29 | } 30 | 31 | output "storage_id" { 32 | value = var.storage_id 33 | } 34 | 35 | output "frontend_id" { 36 | value = var.frontend_id 37 | } 38 | -------------------------------------------------------------------------------- /tests/cdn/providers.tf: -------------------------------------------------------------------------------- 1 | provider "random" { 2 | # Configuration options for the provider (if any) 3 | } 4 | -------------------------------------------------------------------------------- /tests/cluster/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the cluster" 3 | type = string 4 | default = "cluster" 5 | } 6 | 7 | variable "vpc_id" { 8 | description = "Mock input for vpc id" 9 | type = string 10 | default = "" 11 | } 12 | 13 | variable "lb_id" { 14 | description = "Mock input for lb id" 15 | type = string 16 | default = "" 17 | } 18 | 19 | resource "random_id" "id" { 20 | byte_length = 8 21 | } 22 | 23 | locals { 24 | mock_cluster_id = "${var.name}-${random_id.id.hex}" 25 | } 26 | 27 | output "cluster_id" { 28 | value = local.mock_cluster_id 29 | } 30 | 31 | output "vpc_id" { 32 | value = var.vpc_id 33 | } 34 | 35 | output "lb_id" { 36 | value = var.lb_id 37 | } 38 | -------------------------------------------------------------------------------- /tests/cluster/providers.tf: -------------------------------------------------------------------------------- 1 | provider "random" { 2 | # Configuration options for the provider (if any) 3 | } 4 | -------------------------------------------------------------------------------- /tests/database/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the database" 3 | type = string 4 | default = "database" 5 | } 6 | 7 | variable "vpc_id" { 8 | description = "Mock input for vpc id" 9 | type = string 10 | default = "" 11 | } 12 | 13 | resource "random_id" "id" { 14 | byte_length = 8 15 | } 16 | 17 | locals { 18 | mock_database_id = "${var.name}-${random_id.id.hex}" 19 | } 20 | 21 | output "database_id" { 22 | value = local.mock_database_id 23 | } 24 | 25 | output "vpc_id" { 26 | value = var.vpc_id 27 | } 28 | -------------------------------------------------------------------------------- /tests/database/providers.tf: -------------------------------------------------------------------------------- 1 | provider "random" { 2 | # Configuration options for the provider (if any) 3 | } 4 | -------------------------------------------------------------------------------- /tests/dynamodb/README.md: -------------------------------------------------------------------------------- 1 | # Component: `dynamodb` 2 | 3 | This component is responsible for provisioning a DynamoDB table. 4 | 5 | ## Usage 6 | 7 | **Stack Level**: Regional 8 | 9 | Here is an example snippet for how to use this component: 10 | 11 | ```yaml 12 | components: 13 | terraform: 14 | dynamodb: 15 | backend: 16 | s3: 17 | workspace_key_prefix: dynamodb 18 | vars: 19 | enabled: true 20 | hash_key: HashKey 21 | range_key: RangeKey 22 | billing_mode: PAY_PER_REQUEST 23 | autoscaler_enabled: false 24 | encryption_enabled: true 25 | point_in_time_recovery_enabled: true 26 | streams_enabled: false 27 | ttl_enabled: false 28 | ``` 29 | 30 | 31 | 32 | ## Requirements 33 | 34 | | Name | Version | 35 | |------|---------| 36 | | [terraform](#requirement\_terraform) | >= 1.0.0 | 37 | | [aws](#requirement\_aws) | >= 4.9.0 | 38 | 39 | ## Providers 40 | 41 | No providers. 42 | 43 | ## Modules 44 | 45 | | Name | Source | Version | 46 | |------|--------|---------| 47 | | [dynamodb\_table](#module\_dynamodb\_table) | cloudposse/dynamodb/aws | 0.36.0 | 48 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | 49 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | 50 | 51 | ## Resources 52 | 53 | No resources. 54 | 55 | ## Inputs 56 | 57 | | Name | Description | Type | Default | Required | 58 | |------|-------------|------|---------|:--------:| 59 | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | 60 | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | 61 | | [autoscale\_max\_read\_capacity](#input\_autoscale\_max\_read\_capacity) | DynamoDB autoscaling max read capacity | `number` | `20` | no | 62 | | [autoscale\_max\_write\_capacity](#input\_autoscale\_max\_write\_capacity) | DynamoDB autoscaling max write capacity | `number` | `20` | no | 63 | | [autoscale\_min\_read\_capacity](#input\_autoscale\_min\_read\_capacity) | DynamoDB autoscaling min read capacity | `number` | `5` | no | 64 | | [autoscale\_min\_write\_capacity](#input\_autoscale\_min\_write\_capacity) | DynamoDB autoscaling min write capacity | `number` | `5` | no | 65 | | [autoscale\_read\_target](#input\_autoscale\_read\_target) | The target value (in %) for DynamoDB read autoscaling | `number` | `50` | no | 66 | | [autoscale\_write\_target](#input\_autoscale\_write\_target) | The target value (in %) for DynamoDB write autoscaling | `number` | `50` | no | 67 | | [autoscaler\_attributes](#input\_autoscaler\_attributes) | Additional attributes for the autoscaler module | `list(string)` | `[]` | no | 68 | | [autoscaler\_enabled](#input\_autoscaler\_enabled) | Flag to enable/disable DynamoDB autoscaling | `bool` | `false` | no | 69 | | [autoscaler\_tags](#input\_autoscaler\_tags) | Additional resource tags for the autoscaler module | `map(string)` | `{}` | no | 70 | | [billing\_mode](#input\_billing\_mode) | DynamoDB Billing mode. Can be PROVISIONED or PAY\_PER\_REQUEST | `string` | `"PROVISIONED"` | no | 71 | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | 72 | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | 73 | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | 74 | | [dynamodb\_attributes](#input\_dynamodb\_attributes) | Additional DynamoDB attributes in the form of a list of mapped values |
list(object({
name = string
type = string
}))
| `[]` | no | 75 | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | 76 | | [encryption\_enabled](#input\_encryption\_enabled) | Enable DynamoDB server-side encryption | `bool` | `true` | no | 77 | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | 78 | | [global\_secondary\_index\_map](#input\_global\_secondary\_index\_map) | Additional global secondary indexes in the form of a list of mapped values |
list(object({
hash_key = string
name = string
non_key_attributes = list(string)
projection_type = string
range_key = string
read_capacity = number
write_capacity = number
}))
| `[]` | no | 79 | | [hash\_key](#input\_hash\_key) | DynamoDB table Hash Key | `string` | n/a | yes | 80 | | [hash\_key\_type](#input\_hash\_key\_type) | Hash Key type, which must be a scalar type: `S`, `N`, or `B` for String, Number or Binary data, respectively. | `string` | `"S"` | no | 81 | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | 82 | | [import\_table](#input\_import\_table) | Import Amazon S3 data into a new table. |
object({
# Valid values are GZIP, ZSTD and NONE
input_compression_type = optional(string, null)
# Valid values are CSV, DYNAMODB_JSON, and ION.
input_format = string
input_format_options = optional(object({
csv = object({
delimiter = string
header_list = list(string)
})
}), null)
s3_bucket_source = object({
bucket = string
bucket_owner = optional(string)
key_prefix = optional(string)
})
})
| `null` | no | 83 | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | 84 | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | 85 | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | 86 | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | 87 | | [local\_secondary\_index\_map](#input\_local\_secondary\_index\_map) | Additional local secondary indexes in the form of a list of mapped values |
list(object({
name = string
non_key_attributes = list(string)
projection_type = string
range_key = string
}))
| `[]` | no | 88 | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | 89 | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | 90 | | [point\_in\_time\_recovery\_enabled](#input\_point\_in\_time\_recovery\_enabled) | Enable DynamoDB point in time recovery | `bool` | `true` | no | 91 | | [range\_key](#input\_range\_key) | DynamoDB table Range Key | `string` | `""` | no | 92 | | [range\_key\_type](#input\_range\_key\_type) | Range Key type, which must be a scalar type: `S`, `N`, or `B` for String, Number or Binary data, respectively. | `string` | `"S"` | no | 93 | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | 94 | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | 95 | | [replicas](#input\_replicas) | List of regions to create a replica table in | `list(string)` | `[]` | no | 96 | | [server\_side\_encryption\_kms\_key\_arn](#input\_server\_side\_encryption\_kms\_key\_arn) | The ARN of the CMK that should be used for the AWS KMS encryption. This attribute should only be specified if the key is different from the default DynamoDB CMK, alias/aws/dynamodb. | `string` | `null` | no | 97 | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | 98 | | [stream\_view\_type](#input\_stream\_view\_type) | When an item in the table is modified, what information is written to the stream | `string` | `""` | no | 99 | | [streams\_enabled](#input\_streams\_enabled) | Enable DynamoDB streams | `bool` | `false` | no | 100 | | [table\_name](#input\_table\_name) | Table name. If provided, the bucket will be created with this name instead of generating the name from the context | `string` | `null` | no | 101 | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | 102 | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | 103 | | [ttl\_attribute](#input\_ttl\_attribute) | DynamoDB table TTL attribute | `string` | `""` | no | 104 | | [ttl\_enabled](#input\_ttl\_enabled) | Set to false to disable DynamoDB table TTL | `bool` | `false` | no | 105 | 106 | ## Outputs 107 | 108 | | Name | Description | 109 | |------|-------------| 110 | | [global\_secondary\_index\_names](#output\_global\_secondary\_index\_names) | DynamoDB global secondary index names | 111 | | [hash\_key](#output\_hash\_key) | DynamoDB table hash key | 112 | | [local\_secondary\_index\_names](#output\_local\_secondary\_index\_names) | DynamoDB local secondary index names | 113 | | [range\_key](#output\_range\_key) | DynamoDB table range key | 114 | | [table\_arn](#output\_table\_arn) | DynamoDB table ARN | 115 | | [table\_id](#output\_table\_id) | DynamoDB table ID | 116 | | [table\_name](#output\_table\_name) | DynamoDB table name | 117 | | [table\_stream\_arn](#output\_table\_stream\_arn) | DynamoDB table stream ARN | 118 | | [table\_stream\_label](#output\_table\_stream\_label) | DynamoDB table stream label | 119 | 120 | 121 | 122 | ## References 123 | 124 | - [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dynamodb) - 125 | Cloud Posse's upstream component 126 | 127 | [](https://cpco.io/component) 128 | -------------------------------------------------------------------------------- /tests/dynamodb/component.yaml: -------------------------------------------------------------------------------- 1 | # 'dynamodb' component vendoring config 2 | 3 | # 'component.yaml' in the component folder is processed by the 'atmos' commands 4 | # 'atmos vendor pull -c dynamodb' or 'atmos vendor pull --component dynamodb' 5 | 6 | apiVersion: atmos/v1 7 | kind: ComponentVendorConfig 8 | spec: 9 | source: 10 | uri: github.com/cloudposse/terraform-aws-components.git//modules/dynamodb?ref={{ .Version }} 11 | version: 1.462.0 12 | included_paths: 13 | - "**/**" 14 | excluded_paths: [] 15 | mixins: [] 16 | -------------------------------------------------------------------------------- /tests/dynamodb/context.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 3 | # All other instances of this file should be a copy of that one 4 | # 5 | # 6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 7 | # and then place it in your Terraform module to automatically get 8 | # Cloud Posse's standard configuration inputs suitable for passing 9 | # to Cloud Posse modules. 10 | # 11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf 12 | # 13 | # Modules should access the whole context as `module.this.context` 14 | # to get the input variables with nulls for defaults, 15 | # for example `context = module.this.context`, 16 | # and access individual variables as `module.this.`, 17 | # with final values filled in. 18 | # 19 | # For example, when using defaults, `module.this.context.delimiter` 20 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 21 | # 22 | 23 | module "this" { 24 | source = "cloudposse/label/null" 25 | version = "0.25.0" # requires Terraform >= 0.13.0 26 | 27 | enabled = var.enabled 28 | namespace = var.namespace 29 | tenant = var.tenant 30 | environment = var.environment 31 | stage = var.stage 32 | name = var.name 33 | delimiter = var.delimiter 34 | attributes = var.attributes 35 | tags = var.tags 36 | additional_tag_map = var.additional_tag_map 37 | label_order = var.label_order 38 | regex_replace_chars = var.regex_replace_chars 39 | id_length_limit = var.id_length_limit 40 | label_key_case = var.label_key_case 41 | label_value_case = var.label_value_case 42 | descriptor_formats = var.descriptor_formats 43 | labels_as_tags = var.labels_as_tags 44 | 45 | context = var.context 46 | } 47 | 48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 49 | 50 | variable "context" { 51 | type = any 52 | default = { 53 | enabled = true 54 | namespace = null 55 | tenant = null 56 | environment = null 57 | stage = null 58 | name = null 59 | delimiter = null 60 | attributes = [] 61 | tags = {} 62 | additional_tag_map = {} 63 | regex_replace_chars = null 64 | label_order = [] 65 | id_length_limit = null 66 | label_key_case = null 67 | label_value_case = null 68 | descriptor_formats = {} 69 | # Note: we have to use [] instead of null for unset lists due to 70 | # https://github.com/hashicorp/terraform/issues/28137 71 | # which was not fixed until Terraform 1.0.0, 72 | # but we want the default to be all the labels in `label_order` 73 | # and we want users to be able to prevent all tag generation 74 | # by setting `labels_as_tags` to `[]`, so we need 75 | # a different sentinel to indicate "default" 76 | labels_as_tags = ["unset"] 77 | } 78 | description = <<-EOT 79 | Single object for setting entire context at once. 80 | See description of individual variables for details. 81 | Leave string and numeric variables as `null` to use default value. 82 | Individual variable settings (non-null) override settings in context object, 83 | except for attributes, tags, and additional_tag_map, which are merged. 84 | EOT 85 | 86 | validation { 87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 88 | error_message = "Allowed values: `lower`, `title`, `upper`." 89 | } 90 | 91 | validation { 92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 94 | } 95 | } 96 | 97 | variable "enabled" { 98 | type = bool 99 | default = null 100 | description = "Set to false to prevent the module from creating any resources" 101 | } 102 | 103 | variable "namespace" { 104 | type = string 105 | default = null 106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 107 | } 108 | 109 | variable "tenant" { 110 | type = string 111 | default = null 112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 113 | } 114 | 115 | variable "environment" { 116 | type = string 117 | default = null 118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 119 | } 120 | 121 | variable "stage" { 122 | type = string 123 | default = null 124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 125 | } 126 | 127 | variable "name" { 128 | type = string 129 | default = null 130 | description = <<-EOT 131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 132 | This is the only ID element not also included as a `tag`. 133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 134 | EOT 135 | } 136 | 137 | variable "delimiter" { 138 | type = string 139 | default = null 140 | description = <<-EOT 141 | Delimiter to be used between ID elements. 142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 143 | EOT 144 | } 145 | 146 | variable "attributes" { 147 | type = list(string) 148 | default = [] 149 | description = <<-EOT 150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 151 | in the order they appear in the list. New attributes are appended to the 152 | end of the list. The elements of the list are joined by the `delimiter` 153 | and treated as a single ID element. 154 | EOT 155 | } 156 | 157 | variable "labels_as_tags" { 158 | type = set(string) 159 | default = ["default"] 160 | description = <<-EOT 161 | Set of labels (ID elements) to include as tags in the `tags` output. 162 | Default is to include all labels. 163 | Tags with empty values will not be included in the `tags` output. 164 | Set to `[]` to suppress all generated tags. 165 | **Notes:** 166 | The value of the `name` tag, if included, will be the `id`, not the `name`. 167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 168 | changed in later chained modules. Attempts to change it will be silently ignored. 169 | EOT 170 | } 171 | 172 | variable "tags" { 173 | type = map(string) 174 | default = {} 175 | description = <<-EOT 176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 177 | Neither the tag keys nor the tag values will be modified by this module. 178 | EOT 179 | } 180 | 181 | variable "additional_tag_map" { 182 | type = map(string) 183 | default = {} 184 | description = <<-EOT 185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 186 | This is for some rare cases where resources want additional configuration of tags 187 | and therefore take a list of maps with tag key, value, and additional configuration. 188 | EOT 189 | } 190 | 191 | variable "label_order" { 192 | type = list(string) 193 | default = null 194 | description = <<-EOT 195 | The order in which the labels (ID elements) appear in the `id`. 196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 198 | EOT 199 | } 200 | 201 | variable "regex_replace_chars" { 202 | type = string 203 | default = null 204 | description = <<-EOT 205 | Terraform regular expression (regex) string. 206 | Characters matching the regex will be removed from the ID elements. 207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 208 | EOT 209 | } 210 | 211 | variable "id_length_limit" { 212 | type = number 213 | default = null 214 | description = <<-EOT 215 | Limit `id` to this many characters (minimum 6). 216 | Set to `0` for unlimited length. 217 | Set to `null` for keep the existing setting, which defaults to `0`. 218 | Does not affect `id_full`. 219 | EOT 220 | validation { 221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 223 | } 224 | } 225 | 226 | variable "label_key_case" { 227 | type = string 228 | default = null 229 | description = <<-EOT 230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 231 | Does not affect keys of tags passed in via the `tags` input. 232 | Possible values: `lower`, `title`, `upper`. 233 | Default value: `title`. 234 | EOT 235 | 236 | validation { 237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 238 | error_message = "Allowed values: `lower`, `title`, `upper`." 239 | } 240 | } 241 | 242 | variable "label_value_case" { 243 | type = string 244 | default = null 245 | description = <<-EOT 246 | Controls the letter case of ID elements (labels) as included in `id`, 247 | set as tag values, and output by this module individually. 248 | Does not affect values of tags passed in via the `tags` input. 249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 251 | Default value: `lower`. 252 | EOT 253 | 254 | validation { 255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 257 | } 258 | } 259 | 260 | variable "descriptor_formats" { 261 | type = any 262 | default = {} 263 | description = <<-EOT 264 | Describe additional descriptors to be output in the `descriptors` output map. 265 | Map of maps. Keys are names of descriptors. Values are maps of the form 266 | `{ 267 | format = string 268 | labels = list(string) 269 | }` 270 | (Type is `any` so the map values can later be enhanced to provide additional options.) 271 | `format` is a Terraform format string to be passed to the `format()` function. 272 | `labels` is a list of labels, in order, to pass to `format()` function. 273 | Label values will be normalized before being passed to `format()` so they will be 274 | identical to how they appear in `id`. 275 | Default is `{}` (`descriptors` output will be empty). 276 | EOT 277 | } 278 | 279 | #### End of copy of cloudposse/terraform-null-label/variables.tf 280 | -------------------------------------------------------------------------------- /tests/dynamodb/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | enabled = module.this.enabled 3 | is_on_demand = local.enabled && var.billing_mode == "PAY_PER_REQUEST" 4 | autoscaler_enabled = !local.is_on_demand && var.autoscaler_enabled 5 | } 6 | 7 | module "dynamodb_table" { 8 | source = "cloudposse/dynamodb/aws" 9 | version = "0.36.0" 10 | 11 | table_name = var.table_name 12 | billing_mode = var.billing_mode 13 | replicas = var.replicas 14 | dynamodb_attributes = var.dynamodb_attributes 15 | import_table = var.import_table 16 | 17 | global_secondary_index_map = var.global_secondary_index_map 18 | local_secondary_index_map = var.local_secondary_index_map 19 | 20 | hash_key = var.hash_key 21 | hash_key_type = var.hash_key_type 22 | range_key = var.range_key 23 | range_key_type = var.range_key_type 24 | 25 | enable_autoscaler = local.autoscaler_enabled 26 | autoscale_write_target = local.autoscaler_enabled ? var.autoscale_write_target : null 27 | autoscale_read_target = local.autoscaler_enabled ? var.autoscale_read_target : null 28 | autoscale_min_read_capacity = local.autoscaler_enabled ? var.autoscale_min_read_capacity : null 29 | autoscale_max_read_capacity = local.autoscaler_enabled ? var.autoscale_max_read_capacity : null 30 | autoscale_min_write_capacity = local.autoscaler_enabled ? var.autoscale_min_write_capacity : null 31 | autoscale_max_write_capacity = local.autoscaler_enabled ? var.autoscale_max_write_capacity : null 32 | autoscaler_attributes = local.autoscaler_enabled ? var.autoscaler_attributes : [] 33 | autoscaler_tags = local.autoscaler_enabled ? var.autoscaler_tags : null 34 | 35 | enable_encryption = var.encryption_enabled 36 | server_side_encryption_kms_key_arn = var.server_side_encryption_kms_key_arn 37 | 38 | enable_streams = var.streams_enabled 39 | stream_view_type = var.stream_view_type 40 | 41 | ttl_enabled = var.ttl_enabled 42 | ttl_attribute = var.ttl_attribute 43 | 44 | enable_point_in_time_recovery = var.point_in_time_recovery_enabled 45 | 46 | context = module.this.context 47 | } 48 | -------------------------------------------------------------------------------- /tests/dynamodb/outputs.tf: -------------------------------------------------------------------------------- 1 | output "table_name" { 2 | value = module.dynamodb_table.table_name 3 | description = "DynamoDB table name" 4 | } 5 | 6 | output "table_id" { 7 | value = module.dynamodb_table.table_id 8 | description = "DynamoDB table ID" 9 | } 10 | 11 | output "table_arn" { 12 | value = module.dynamodb_table.table_arn 13 | description = "DynamoDB table ARN" 14 | } 15 | 16 | output "global_secondary_index_names" { 17 | value = module.dynamodb_table.global_secondary_index_names 18 | description = "DynamoDB global secondary index names" 19 | } 20 | 21 | output "local_secondary_index_names" { 22 | value = module.dynamodb_table.local_secondary_index_names 23 | description = "DynamoDB local secondary index names" 24 | } 25 | 26 | output "table_stream_arn" { 27 | value = module.dynamodb_table.table_stream_arn 28 | description = "DynamoDB table stream ARN" 29 | } 30 | 31 | output "table_stream_label" { 32 | value = module.dynamodb_table.table_stream_label 33 | description = "DynamoDB table stream label" 34 | } 35 | 36 | output "hash_key" { 37 | value = var.hash_key 38 | description = "DynamoDB table hash key" 39 | } 40 | 41 | output "range_key" { 42 | value = var.range_key 43 | description = "DynamoDB table range key" 44 | } 45 | -------------------------------------------------------------------------------- /tests/dynamodb/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | 4 | # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. 5 | profile = module.iam_roles.terraform_profile_name 6 | 7 | dynamic "assume_role" { 8 | # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. 9 | for_each = compact([module.iam_roles.terraform_role_arn]) 10 | content { 11 | role_arn = assume_role.value 12 | } 13 | } 14 | } 15 | 16 | module "iam_roles" { 17 | source = "../account-map/modules/iam-roles" 18 | context = module.this.context 19 | } 20 | -------------------------------------------------------------------------------- /tests/dynamodb/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | type = string 3 | description = "AWS Region." 4 | } 5 | 6 | variable "autoscale_write_target" { 7 | type = number 8 | default = 50 9 | description = "The target value (in %) for DynamoDB write autoscaling" 10 | } 11 | 12 | variable "autoscale_read_target" { 13 | type = number 14 | default = 50 15 | description = "The target value (in %) for DynamoDB read autoscaling" 16 | } 17 | 18 | variable "autoscale_min_read_capacity" { 19 | type = number 20 | default = 5 21 | description = "DynamoDB autoscaling min read capacity" 22 | } 23 | 24 | variable "autoscale_max_read_capacity" { 25 | type = number 26 | default = 20 27 | description = "DynamoDB autoscaling max read capacity" 28 | } 29 | 30 | variable "autoscale_min_write_capacity" { 31 | type = number 32 | default = 5 33 | description = "DynamoDB autoscaling min write capacity" 34 | } 35 | 36 | variable "autoscale_max_write_capacity" { 37 | type = number 38 | default = 20 39 | description = "DynamoDB autoscaling max write capacity" 40 | } 41 | 42 | variable "billing_mode" { 43 | type = string 44 | default = "PROVISIONED" 45 | description = "DynamoDB Billing mode. Can be PROVISIONED or PAY_PER_REQUEST" 46 | } 47 | 48 | variable "streams_enabled" { 49 | type = bool 50 | default = false 51 | description = "Enable DynamoDB streams" 52 | } 53 | 54 | variable "stream_view_type" { 55 | type = string 56 | default = "" 57 | description = "When an item in the table is modified, what information is written to the stream" 58 | } 59 | 60 | variable "encryption_enabled" { 61 | type = bool 62 | default = true 63 | description = "Enable DynamoDB server-side encryption" 64 | } 65 | 66 | variable "server_side_encryption_kms_key_arn" { 67 | type = string 68 | default = null 69 | description = "The ARN of the CMK that should be used for the AWS KMS encryption. This attribute should only be specified if the key is different from the default DynamoDB CMK, alias/aws/dynamodb." 70 | } 71 | 72 | variable "point_in_time_recovery_enabled" { 73 | type = bool 74 | default = true 75 | description = "Enable DynamoDB point in time recovery" 76 | } 77 | 78 | variable "hash_key" { 79 | type = string 80 | description = "DynamoDB table Hash Key" 81 | } 82 | 83 | variable "hash_key_type" { 84 | type = string 85 | default = "S" 86 | description = "Hash Key type, which must be a scalar type: `S`, `N`, or `B` for String, Number or Binary data, respectively." 87 | } 88 | 89 | variable "range_key" { 90 | type = string 91 | default = "" 92 | description = "DynamoDB table Range Key" 93 | } 94 | 95 | variable "range_key_type" { 96 | type = string 97 | default = "S" 98 | description = "Range Key type, which must be a scalar type: `S`, `N`, or `B` for String, Number or Binary data, respectively." 99 | } 100 | 101 | variable "ttl_attribute" { 102 | type = string 103 | default = "" 104 | description = "DynamoDB table TTL attribute" 105 | } 106 | 107 | variable "ttl_enabled" { 108 | type = bool 109 | default = false 110 | description = "Set to false to disable DynamoDB table TTL" 111 | } 112 | 113 | variable "autoscaler_enabled" { 114 | type = bool 115 | default = false 116 | description = "Flag to enable/disable DynamoDB autoscaling" 117 | } 118 | 119 | variable "autoscaler_attributes" { 120 | type = list(string) 121 | default = [] 122 | description = "Additional attributes for the autoscaler module" 123 | } 124 | 125 | variable "autoscaler_tags" { 126 | type = map(string) 127 | default = {} 128 | description = "Additional resource tags for the autoscaler module" 129 | } 130 | 131 | variable "table_name" { 132 | type = string 133 | default = null 134 | description = "Table name. If provided, the bucket will be created with this name instead of generating the name from the context" 135 | } 136 | 137 | variable "dynamodb_attributes" { 138 | type = list(object({ 139 | name = string 140 | type = string 141 | })) 142 | default = [] 143 | description = "Additional DynamoDB attributes in the form of a list of mapped values" 144 | } 145 | 146 | variable "global_secondary_index_map" { 147 | type = list(object({ 148 | hash_key = string 149 | name = string 150 | non_key_attributes = list(string) 151 | projection_type = string 152 | range_key = string 153 | read_capacity = number 154 | write_capacity = number 155 | })) 156 | default = [] 157 | description = "Additional global secondary indexes in the form of a list of mapped values" 158 | } 159 | 160 | variable "local_secondary_index_map" { 161 | type = list(object({ 162 | name = string 163 | non_key_attributes = list(string) 164 | projection_type = string 165 | range_key = string 166 | })) 167 | default = [] 168 | description = "Additional local secondary indexes in the form of a list of mapped values" 169 | } 170 | 171 | variable "replicas" { 172 | type = list(string) 173 | default = [] 174 | description = "List of regions to create a replica table in" 175 | } 176 | 177 | variable "import_table" { 178 | type = object({ 179 | # Valid values are GZIP, ZSTD and NONE 180 | input_compression_type = optional(string, null) 181 | # Valid values are CSV, DYNAMODB_JSON, and ION. 182 | input_format = string 183 | input_format_options = optional(object({ 184 | csv = object({ 185 | delimiter = string 186 | header_list = list(string) 187 | }) 188 | }), null) 189 | s3_bucket_source = object({ 190 | bucket = string 191 | bucket_owner = optional(string) 192 | key_prefix = optional(string) 193 | }) 194 | }) 195 | default = null 196 | description = "Import Amazon S3 data into a new table." 197 | } 198 | -------------------------------------------------------------------------------- /tests/dynamodb/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 4.9.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/frontend/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the frontend" 3 | type = string 4 | default = "frontend" 5 | } 6 | 7 | variable "api_id" { 8 | description = "Mock input for api id" 9 | type = string 10 | default = "" 11 | } 12 | 13 | variable "cache_id" { 14 | description = "Mock input for cache id" 15 | type = string 16 | default = "" 17 | } 18 | 19 | resource "random_id" "id" { 20 | byte_length = 8 21 | } 22 | 23 | locals { 24 | mock_frontend_id = "${var.name}-${random_id.id.hex}" 25 | } 26 | 27 | output "frontend_id" { 28 | value = local.mock_frontend_id 29 | } 30 | 31 | output "api_id" { 32 | value = var.api_id 33 | } 34 | 35 | output "cache_id" { 36 | value = var.cache_id 37 | } 38 | -------------------------------------------------------------------------------- /tests/frontend/providers.tf: -------------------------------------------------------------------------------- 1 | provider "random" { 2 | # Configuration options for the provider (if any) 3 | } 4 | -------------------------------------------------------------------------------- /tests/github-oidc-provider/README.md: -------------------------------------------------------------------------------- 1 | # `github-oidc-provider` 2 | 3 | This component is not the true `github-oidc-provider` component. This repo is used for demo purposes and therefore is 4 | drastically simplified. 5 | -------------------------------------------------------------------------------- /tests/github-oidc-provider/outputs.tf: -------------------------------------------------------------------------------- 1 | output "oidc_provider_arn" { 2 | description = "mock component output" 3 | value = "arn:aws:iam::461333128641:oidc-provider/token.actions.githubusercontent.com" 4 | } 5 | -------------------------------------------------------------------------------- /tests/github-oidc-provider/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.0" 3 | } 4 | -------------------------------------------------------------------------------- /tests/github-oidc-role/README.md: -------------------------------------------------------------------------------- 1 | # Component: `github-oidc-role` 2 | 3 | This component is responsible for creating IAM roles for GitHub Actions to assume. 4 | 5 | ## Usage 6 | 7 | **Stack Level**: Global 8 | 9 | Here's an example snippet for how to use this component. 10 | 11 | ```yaml 12 | # stacks/catalog/github-oidc-role/defaults.yaml 13 | components: 14 | terraform: 15 | github-oidc-role/defaults: 16 | metadata: 17 | type: abstract 18 | vars: 19 | enabled: true 20 | name: gha-iam 21 | # Note: inherited lists are not merged, they are replaced 22 | github_actions_allowed_repos: 23 | - MyOrg/* ## allow all repos in MyOrg 24 | ``` 25 | 26 | Example using for gitops (predefined policy): 27 | 28 | ```yaml 29 | # stacks/catalog/github-oidc-role/gitops.yaml 30 | import: 31 | - catalog/github-oidc-role/defaults 32 | 33 | components: 34 | terraform: 35 | github-oidc-role/gitops: 36 | metadata: 37 | component: github-oidc-role 38 | inherits: 39 | - github-oidc-role/defaults 40 | vars: 41 | enabled: true 42 | # Note: inherited lists are not merged, they are replaced 43 | github_actions_allowed_repos: 44 | - "MyOrg/infrastructure" 45 | attributes: ["gitops"] 46 | iam_policies: 47 | - gitops 48 | gitops_policy_configuration: 49 | s3_bucket_component_name: gitops/s3-bucket 50 | dynamodb_component_name: gitops/dynamodb 51 | ``` 52 | 53 | Example using for lambda-cicd (predefined policy): 54 | 55 | ```yaml 56 | # stacks/catalog/github-oidc-role/lambda-cicd.yaml 57 | import: 58 | - catalog/github-oidc-role/defaults 59 | 60 | components: 61 | terraform: 62 | github-oidc-role/lambda-cicd: 63 | metadata: 64 | component: github-oidc-role 65 | inherits: 66 | - github-oidc-role/defaults 67 | vars: 68 | enabled: true 69 | github_actions_allowed_repos: 70 | - MyOrg/example-app-on-lambda-with-gha 71 | attributes: ["lambda-cicd"] 72 | iam_policies: 73 | - lambda-cicd 74 | lambda_cicd_policy_configuration: 75 | enable_ssm_access: true 76 | enable_s3_access: true 77 | s3_bucket_component_name: s3-bucket/github-action-artifacts 78 | s3_bucket_environment_name: gbl 79 | s3_bucket_stage_name: artifacts 80 | s3_bucket_tenant_name: core 81 | ``` 82 | 83 | Example Using an AWS Managed policy and a custom inline policy: 84 | 85 | ```yaml 86 | # stacks/catalog/github-oidc-role/custom.yaml 87 | import: 88 | - catalog/github-oidc-role/defaults 89 | 90 | components: 91 | terraform: 92 | github-oidc-role/custom: 93 | metadata: 94 | component: github-oidc-role 95 | inherits: 96 | - github-oidc-role/defaults 97 | vars: 98 | enabled: true 99 | github_actions_allowed_repos: 100 | - MyOrg/example-app-on-lambda-with-gha 101 | attributes: ["custom"] 102 | iam_policies: 103 | - arn:aws:iam::aws:policy/AdministratorAccess 104 | iam_policy: 105 | - version: "2012-10-17" 106 | statements: 107 | - effect: "Allow" 108 | actions: 109 | - "ec2:*" 110 | resources: 111 | - "*" 112 | ``` 113 | 114 | ### Adding Custom Policies 115 | 116 | There are two methods for adding custom policies to the IAM role. 117 | 118 | 1. Through the `iam_policy` input which you can use to add inline policies to the IAM role. 119 | 2. By defining policies in Terraform and then attaching them to roles by name. 120 | 121 | #### Defining Custom Policies in Terraform 122 | 123 | 1. Give the policy a unique name, e.g. `docker-publish`. We will use `NAME` as a placeholder for the name in the 124 | instructions below. 125 | 2. Create a file in the component directory (i.e. `github-oidc-role`) with the name `policy_NAME.tf`. 126 | 3. In that file, conditionally (based on need) create a policy document as follows: 127 | 128 | ```hcl 129 | locals { 130 | NAME_policy_enabled = contains(var.iam_policies, "NAME") 131 | NAME_policy = local.NAME_policy_enabled ? one(data.aws_iam_policy_document.NAME.*.json) : null 132 | } 133 | 134 | data "aws_iam_policy_document" "NAME" { 135 | count = local.NAME_policy_enabled ? 1 : 0 136 | 137 | # Define the policy here 138 | } 139 | ``` 140 | 141 | Note that you can also add input variables and outputs to this file if desired. Just make sure that all inputs are 142 | optional. 143 | 144 | 4. Create a file named `additional-policy-map_override.tf` in the component directory (if it does not already exist). 145 | This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), meaning its 146 | contents will be merged with the main terraform file, and any locals defined in it will override locals defined in 147 | other files. Having your code in this separate override file makes it possible for the component to provide a 148 | placeholder local variable so that it works without customization, while allowing you to customize the component and 149 | still update it without losing your customizations. 150 | 5. In that file, redefine the local variable `overridable_additional_custom_policy_map` map as follows: 151 | 152 | ```hcl 153 | locals { 154 | overridable_additional_custom_policy_map = { 155 | "NAME" = local.NAME_policy 156 | } 157 | } 158 | ``` 159 | 160 | If you have multiple custom policies, using just this one file, add each policy document to the map in the form 161 | `NAME = local.NAME_policy`. 162 | 163 | 6. With that done, you can now attach that policy by adding the name to the `iam_policies` list. For example: 164 | 165 | ```yaml 166 | iam_policies: 167 | - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" 168 | - "NAME" 169 | ``` 170 | 171 | 172 | 173 | ## Requirements 174 | 175 | | Name | Version | 176 | |------|---------| 177 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 178 | | [aws](#requirement\_aws) | >= 4.9.0 | 179 | 180 | ## Providers 181 | 182 | | Name | Version | 183 | |------|---------| 184 | | [aws](#provider\_aws) | >= 4.9.0 | 185 | 186 | ## Modules 187 | 188 | | Name | Source | Version | 189 | |------|--------|---------| 190 | | [dynamodb](#module\_dynamodb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | 191 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | 192 | | [iam\_policy](#module\_iam\_policy) | cloudposse/iam-policy/aws | 2.0.1 | 193 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | 194 | | [s3\_artifacts\_bucket](#module\_s3\_artifacts\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | 195 | | [s3\_bucket](#module\_s3\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | 196 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | 197 | 198 | ## Resources 199 | 200 | | Name | Type | 201 | |------|------| 202 | | [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 203 | | [aws_iam_policy_document.gitops_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 204 | | [aws_iam_policy_document.lambda_cicd_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 205 | 206 | ## Inputs 207 | 208 | | Name | Description | Type | Default | Required | 209 | |------|-------------|------|---------|:--------:| 210 | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | 211 | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | 212 | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | 213 | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | 214 | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | 215 | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | 216 | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | 217 | | [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | 218 | | [gitops\_policy\_configuration](#input\_gitops\_policy\_configuration) | Configuration for the GitOps IAM Policy, valid keys are
- `s3_bucket_component_name` - Component Name of where to store the TF Plans in S3, defaults to `gitops/s3-bucket`
- `dynamodb_component_name` - Component Name of where to store the TF Plans in Dynamodb, defaults to `gitops/dynamodb`
- `s3_bucket_environment_name` - Environment name for the S3 Bucket, defaults to current environment
- `dynamodb_environment_name` - Environment name for the Dynamodb Table, defaults to current environment |
object({
s3_bucket_component_name = optional(string, "gitops/s3-bucket")
dynamodb_component_name = optional(string, "gitops/dynamodb")
s3_bucket_environment_name = optional(string)
dynamodb_environment_name = optional(string)
})
| `{}` | no | 219 | | [iam\_policies](#input\_iam\_policies) | List of policies to attach to the IAM role, should be either an ARN of an AWS Managed Policy or a name of a custom policy e.g. `gitops` | `list(string)` | `[]` | no | 220 | | [iam\_policy](#input\_iam\_policy) | IAM policy as list of Terraform objects, compatible with Terraform `aws_iam_policy_document` data source
except that `source_policy_documents` and `override_policy_documents` are not included.
Use inputs `iam_source_policy_documents` and `iam_override_policy_documents` for that. |
list(object({
policy_id = optional(string, null)
version = optional(string, null)
statements = list(object({
sid = optional(string, null)
effect = optional(string, null)
actions = optional(list(string), null)
not_actions = optional(list(string), null)
resources = optional(list(string), null)
not_resources = optional(list(string), null)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
not_principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
}))
}))
| `[]` | no | 221 | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | 222 | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | 223 | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | 224 | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | 225 | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | 226 | | [lambda\_cicd\_policy\_configuration](#input\_lambda\_cicd\_policy\_configuration) | Configuration for the lambda-cicd policy. The following keys are supported:
- `enable_kms_access` - (bool) - Whether to allow access to KMS. Defaults to false.
- `enable_ssm_access` - (bool) - Whether to allow access to SSM. Defaults to false.
- `enable_s3_access` - (bool) - Whether to allow access to S3. Defaults to false.
- `s3_bucket_component_name` - (string) - The name of the component to use for the S3 bucket. Defaults to `s3-bucket/github-action-artifacts`.
- `s3_bucket_environment_name` - (string) - The name of the environment to use for the S3 bucket. Defaults to the environment of the current module.
- `s3_bucket_tenant_name` - (string) - The name of the tenant to use for the S3 bucket. Defaults to the tenant of the current module.
- `s3_bucket_stage_name` - (string) - The name of the stage to use for the S3 bucket. Defaults to the stage of the current module.
- `enable_lambda_update` - (bool) - Whether to allow access to update lambda functions. Defaults to false. |
object({
enable_kms_access = optional(bool, false)
enable_ssm_access = optional(bool, false)
enable_s3_access = optional(bool, false)
s3_bucket_component_name = optional(string, "s3-bucket/github-action-artifacts")
s3_bucket_environment_name = optional(string)
s3_bucket_tenant_name = optional(string)
s3_bucket_stage_name = optional(string)
enable_lambda_update = optional(bool, false)
})
| `{}` | no | 227 | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | 228 | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | 229 | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | 230 | | [region](#input\_region) | AWS Region | `string` | n/a | yes | 231 | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | 232 | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | 233 | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | 234 | 235 | ## Outputs 236 | 237 | | Name | Description | 238 | |------|-------------| 239 | | [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | 240 | | [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | 241 | 242 | 243 | 244 | ## References 245 | 246 | - [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-oidc-role) - 247 | Cloud Posse's upstream component 248 | 249 | [](https://cpco.io/component) 250 | -------------------------------------------------------------------------------- /tests/github-oidc-role/additional-policy-map.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # If you have custom policies, override this declaration by creating 3 | # a file called `additional-policy-map_override.tf`. 4 | # Then add the custom policies to the overridable_additional_custom_policy_map in that file. 5 | # The key should be the policy you want to override, the value is the json policy document. 6 | # See the README in `github-oidc-role` for more details. 7 | overridable_additional_custom_policy_map = { 8 | # Example: 9 | # gitops = aws_iam_policy.my_custom_gitops_policy.policy 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/github-oidc-role/component.yaml: -------------------------------------------------------------------------------- 1 | # 'github-oidc-role' component vendoring config 2 | 3 | # 'component.yaml' in the component folder is processed by the 'atmos' commands 4 | # 'atmos vendor pull -c github-oidc-role' or 'atmos vendor pull --component github-oidc-role' 5 | 6 | apiVersion: atmos/v1 7 | kind: ComponentVendorConfig 8 | spec: 9 | source: 10 | uri: github.com/cloudposse/terraform-aws-components.git//modules/github-oidc-role?ref={{ .Version }} 11 | version: 1.462.0 12 | included_paths: 13 | - "**/**" 14 | excluded_paths: [] 15 | mixins: [] 16 | -------------------------------------------------------------------------------- /tests/github-oidc-role/context.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 3 | # All other instances of this file should be a copy of that one 4 | # 5 | # 6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 7 | # and then place it in your Terraform module to automatically get 8 | # Cloud Posse's standard configuration inputs suitable for passing 9 | # to Cloud Posse modules. 10 | # 11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf 12 | # 13 | # Modules should access the whole context as `module.this.context` 14 | # to get the input variables with nulls for defaults, 15 | # for example `context = module.this.context`, 16 | # and access individual variables as `module.this.`, 17 | # with final values filled in. 18 | # 19 | # For example, when using defaults, `module.this.context.delimiter` 20 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 21 | # 22 | 23 | module "this" { 24 | source = "cloudposse/label/null" 25 | version = "0.25.0" # requires Terraform >= 0.13.0 26 | 27 | enabled = var.enabled 28 | namespace = var.namespace 29 | tenant = var.tenant 30 | environment = var.environment 31 | stage = var.stage 32 | name = var.name 33 | delimiter = var.delimiter 34 | attributes = var.attributes 35 | tags = var.tags 36 | additional_tag_map = var.additional_tag_map 37 | label_order = var.label_order 38 | regex_replace_chars = var.regex_replace_chars 39 | id_length_limit = var.id_length_limit 40 | label_key_case = var.label_key_case 41 | label_value_case = var.label_value_case 42 | descriptor_formats = var.descriptor_formats 43 | labels_as_tags = var.labels_as_tags 44 | 45 | context = var.context 46 | } 47 | 48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 49 | 50 | variable "context" { 51 | type = any 52 | default = { 53 | enabled = true 54 | namespace = null 55 | tenant = null 56 | environment = null 57 | stage = null 58 | name = null 59 | delimiter = null 60 | attributes = [] 61 | tags = {} 62 | additional_tag_map = {} 63 | regex_replace_chars = null 64 | label_order = [] 65 | id_length_limit = null 66 | label_key_case = null 67 | label_value_case = null 68 | descriptor_formats = {} 69 | # Note: we have to use [] instead of null for unset lists due to 70 | # https://github.com/hashicorp/terraform/issues/28137 71 | # which was not fixed until Terraform 1.0.0, 72 | # but we want the default to be all the labels in `label_order` 73 | # and we want users to be able to prevent all tag generation 74 | # by setting `labels_as_tags` to `[]`, so we need 75 | # a different sentinel to indicate "default" 76 | labels_as_tags = ["unset"] 77 | } 78 | description = <<-EOT 79 | Single object for setting entire context at once. 80 | See description of individual variables for details. 81 | Leave string and numeric variables as `null` to use default value. 82 | Individual variable settings (non-null) override settings in context object, 83 | except for attributes, tags, and additional_tag_map, which are merged. 84 | EOT 85 | 86 | validation { 87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 88 | error_message = "Allowed values: `lower`, `title`, `upper`." 89 | } 90 | 91 | validation { 92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 94 | } 95 | } 96 | 97 | variable "enabled" { 98 | type = bool 99 | default = null 100 | description = "Set to false to prevent the module from creating any resources" 101 | } 102 | 103 | variable "namespace" { 104 | type = string 105 | default = null 106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 107 | } 108 | 109 | variable "tenant" { 110 | type = string 111 | default = null 112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 113 | } 114 | 115 | variable "environment" { 116 | type = string 117 | default = null 118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 119 | } 120 | 121 | variable "stage" { 122 | type = string 123 | default = null 124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 125 | } 126 | 127 | variable "name" { 128 | type = string 129 | default = null 130 | description = <<-EOT 131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 132 | This is the only ID element not also included as a `tag`. 133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 134 | EOT 135 | } 136 | 137 | variable "delimiter" { 138 | type = string 139 | default = null 140 | description = <<-EOT 141 | Delimiter to be used between ID elements. 142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 143 | EOT 144 | } 145 | 146 | variable "attributes" { 147 | type = list(string) 148 | default = [] 149 | description = <<-EOT 150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 151 | in the order they appear in the list. New attributes are appended to the 152 | end of the list. The elements of the list are joined by the `delimiter` 153 | and treated as a single ID element. 154 | EOT 155 | } 156 | 157 | variable "labels_as_tags" { 158 | type = set(string) 159 | default = ["default"] 160 | description = <<-EOT 161 | Set of labels (ID elements) to include as tags in the `tags` output. 162 | Default is to include all labels. 163 | Tags with empty values will not be included in the `tags` output. 164 | Set to `[]` to suppress all generated tags. 165 | **Notes:** 166 | The value of the `name` tag, if included, will be the `id`, not the `name`. 167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 168 | changed in later chained modules. Attempts to change it will be silently ignored. 169 | EOT 170 | } 171 | 172 | variable "tags" { 173 | type = map(string) 174 | default = {} 175 | description = <<-EOT 176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 177 | Neither the tag keys nor the tag values will be modified by this module. 178 | EOT 179 | } 180 | 181 | variable "additional_tag_map" { 182 | type = map(string) 183 | default = {} 184 | description = <<-EOT 185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 186 | This is for some rare cases where resources want additional configuration of tags 187 | and therefore take a list of maps with tag key, value, and additional configuration. 188 | EOT 189 | } 190 | 191 | variable "label_order" { 192 | type = list(string) 193 | default = null 194 | description = <<-EOT 195 | The order in which the labels (ID elements) appear in the `id`. 196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 198 | EOT 199 | } 200 | 201 | variable "regex_replace_chars" { 202 | type = string 203 | default = null 204 | description = <<-EOT 205 | Terraform regular expression (regex) string. 206 | Characters matching the regex will be removed from the ID elements. 207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 208 | EOT 209 | } 210 | 211 | variable "id_length_limit" { 212 | type = number 213 | default = null 214 | description = <<-EOT 215 | Limit `id` to this many characters (minimum 6). 216 | Set to `0` for unlimited length. 217 | Set to `null` for keep the existing setting, which defaults to `0`. 218 | Does not affect `id_full`. 219 | EOT 220 | validation { 221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 223 | } 224 | } 225 | 226 | variable "label_key_case" { 227 | type = string 228 | default = null 229 | description = <<-EOT 230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 231 | Does not affect keys of tags passed in via the `tags` input. 232 | Possible values: `lower`, `title`, `upper`. 233 | Default value: `title`. 234 | EOT 235 | 236 | validation { 237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 238 | error_message = "Allowed values: `lower`, `title`, `upper`." 239 | } 240 | } 241 | 242 | variable "label_value_case" { 243 | type = string 244 | default = null 245 | description = <<-EOT 246 | Controls the letter case of ID elements (labels) as included in `id`, 247 | set as tag values, and output by this module individually. 248 | Does not affect values of tags passed in via the `tags` input. 249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 251 | Default value: `lower`. 252 | EOT 253 | 254 | validation { 255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 257 | } 258 | } 259 | 260 | variable "descriptor_formats" { 261 | type = any 262 | default = {} 263 | description = <<-EOT 264 | Describe additional descriptors to be output in the `descriptors` output map. 265 | Map of maps. Keys are names of descriptors. Values are maps of the form 266 | `{ 267 | format = string 268 | labels = list(string) 269 | }` 270 | (Type is `any` so the map values can later be enhanced to provide additional options.) 271 | `format` is a Terraform format string to be passed to the `format()` function. 272 | `labels` is a list of labels, in order, to pass to `format()` function. 273 | Label values will be normalized before being passed to `format()` so they will be 274 | identical to how they appear in `id`. 275 | Default is `{}` (`descriptors` output will be empty). 276 | EOT 277 | } 278 | 279 | #### End of copy of cloudposse/terraform-null-label/variables.tf 280 | -------------------------------------------------------------------------------- /tests/github-oidc-role/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | enabled = module.this.enabled 3 | managed_policies = [for arn in var.iam_policies : arn if can(regex("^arn:aws[^:]*:iam::aws:policy/", arn))] 4 | policies = length(local.managed_policies) > 0 ? local.managed_policies : null 5 | policy_document_map = { 6 | "gitops" = local.gitops_policy 7 | "lambda_cicd" = local.lambda_cicd_policy 8 | "inline_policy" = one(module.iam_policy.*.json) 9 | } 10 | custom_policy_map = merge(local.policy_document_map, local.overridable_additional_custom_policy_map) 11 | 12 | # Ignore empty policies of the form `"{}"` as well as null policies 13 | active_policy_map = { for k, v in local.custom_policy_map : k => v if try(length(v), 0) > 3 } 14 | } 15 | 16 | module "iam_policy" { 17 | enabled = local.enabled && length(var.iam_policy) > 0 18 | 19 | source = "cloudposse/iam-policy/aws" 20 | version = "2.0.1" 21 | 22 | iam_policy = var.iam_policy 23 | 24 | context = module.this.context 25 | } 26 | 27 | module "gha_assume_role" { 28 | source = "../account-map/modules/team-assume-role-policy" 29 | 30 | trusted_github_repos = var.github_actions_allowed_repos 31 | 32 | context = module.this.context 33 | } 34 | 35 | resource "aws_iam_role" "github_actions" { 36 | count = local.enabled ? 1 : 0 37 | 38 | name = module.this.id 39 | assume_role_policy = module.gha_assume_role.github_assume_role_policy 40 | 41 | managed_policy_arns = local.policies 42 | 43 | dynamic "inline_policy" { 44 | for_each = local.active_policy_map 45 | content { 46 | name = inline_policy.key 47 | policy = inline_policy.value 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/github-oidc-role/outputs.tf: -------------------------------------------------------------------------------- 1 | output "github_actions_iam_role_arn" { 2 | value = one(aws_iam_role.github_actions[*].arn) 3 | description = "ARN of IAM role for GitHub Actions" 4 | } 5 | 6 | output "github_actions_iam_role_name" { 7 | value = one(aws_iam_role.github_actions[*].name) 8 | description = "Name of IAM role for GitHub Actions" 9 | } 10 | -------------------------------------------------------------------------------- /tests/github-oidc-role/policy_gitops.tf: -------------------------------------------------------------------------------- 1 | variable "gitops_policy_configuration" { 2 | type = object({ 3 | s3_bucket_component_name = optional(string, "gitops/s3-bucket") 4 | dynamodb_component_name = optional(string, "gitops/dynamodb") 5 | s3_bucket_environment_name = optional(string) 6 | dynamodb_environment_name = optional(string) 7 | }) 8 | default = {} 9 | nullable = false 10 | description = <<-EOT 11 | Configuration for the GitOps IAM Policy, valid keys are 12 | - `s3_bucket_component_name` - Component Name of where to store the TF Plans in S3, defaults to `gitops/s3-bucket` 13 | - `dynamodb_component_name` - Component Name of where to store the TF Plans in Dynamodb, defaults to `gitops/dynamodb` 14 | - `s3_bucket_environment_name` - Environment name for the S3 Bucket, defaults to current environment 15 | - `dynamodb_environment_name` - Environment name for the Dynamodb Table, defaults to current environment 16 | EOT 17 | } 18 | 19 | locals { 20 | gitops_policy_enabled = contains(var.iam_policies, "gitops") 21 | gitops_policy = local.gitops_policy_enabled ? one(data.aws_iam_policy_document.gitops_iam_policy.*.json) : null 22 | 23 | s3_bucket_arn = one(module.s3_bucket[*].outputs.bucket_arn) 24 | dynamodb_table_arn = one(module.dynamodb[*].outputs.table_arn) 25 | } 26 | 27 | module "s3_bucket" { 28 | count = local.gitops_policy_enabled ? 1 : 0 29 | 30 | source = "cloudposse/stack-config/yaml//modules/remote-state" 31 | version = "1.5.0" 32 | 33 | component = lookup(var.gitops_policy_configuration, "s3_bucket_component_name", "gitops/s3-bucket") 34 | environment = lookup(var.gitops_policy_configuration, "s3_bucket_environment_name", module.this.environment) 35 | 36 | context = module.this.context 37 | } 38 | 39 | module "dynamodb" { 40 | count = local.gitops_policy_enabled ? 1 : 0 41 | 42 | source = "cloudposse/stack-config/yaml//modules/remote-state" 43 | version = "1.5.0" 44 | 45 | component = lookup(var.gitops_policy_configuration, "dynamodb_component_name", module.this.environment) 46 | environment = lookup(var.gitops_policy_configuration, "dynamodb_environment_name", module.this.environment) 47 | 48 | context = module.this.context 49 | } 50 | 51 | data "aws_iam_policy_document" "gitops_iam_policy" { 52 | count = local.gitops_policy_enabled ? 1 : 0 53 | 54 | # Allow access to the Dynamodb table used to store TF Plans 55 | # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_dynamodb_specific-table.html 56 | statement { 57 | sid = "AllowDynamodbAccess" 58 | effect = "Allow" 59 | actions = [ 60 | "dynamodb:List*", 61 | "dynamodb:DescribeReservedCapacity*", 62 | "dynamodb:DescribeLimits", 63 | "dynamodb:DescribeTimeToLive" 64 | ] 65 | resources = [ 66 | "*" 67 | ] 68 | } 69 | statement { 70 | sid = "AllowDynamodbTableAccess" 71 | effect = "Allow" 72 | actions = [ 73 | "dynamodb:BatchGet*", 74 | "dynamodb:DescribeStream", 75 | "dynamodb:DescribeTable", 76 | "dynamodb:Get*", 77 | "dynamodb:Query", 78 | "dynamodb:Scan", 79 | "dynamodb:BatchWrite*", 80 | "dynamodb:CreateTable", 81 | "dynamodb:Delete*", 82 | "dynamodb:Update*", 83 | "dynamodb:PutItem" 84 | ] 85 | resources = [ 86 | local.dynamodb_table_arn, 87 | "${local.dynamodb_table_arn}/*" 88 | ] 89 | } 90 | 91 | # Allow access to the S3 Bucket used to store TF Plans 92 | # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html 93 | statement { 94 | sid = "AllowS3Actions" 95 | effect = "Allow" 96 | actions = [ 97 | "s3:ListBucket" 98 | ] 99 | resources = [ 100 | local.s3_bucket_arn 101 | ] 102 | } 103 | statement { 104 | sid = "AllowS3ObjectActions" 105 | effect = "Allow" 106 | actions = [ 107 | "s3:*Object" 108 | ] 109 | resources = [ 110 | "${local.s3_bucket_arn}/*" 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/github-oidc-role/policy_lambda-cicd.tf: -------------------------------------------------------------------------------- 1 | variable "lambda_cicd_policy_configuration" { 2 | type = object({ 3 | enable_kms_access = optional(bool, false) 4 | enable_ssm_access = optional(bool, false) 5 | enable_s3_access = optional(bool, false) 6 | s3_bucket_component_name = optional(string, "s3-bucket/github-action-artifacts") 7 | s3_bucket_environment_name = optional(string) 8 | s3_bucket_tenant_name = optional(string) 9 | s3_bucket_stage_name = optional(string) 10 | enable_lambda_update = optional(bool, false) 11 | }) 12 | default = {} 13 | nullable = false 14 | description = <<-EOT 15 | Configuration for the lambda-cicd policy. The following keys are supported: 16 | - `enable_kms_access` - (bool) - Whether to allow access to KMS. Defaults to false. 17 | - `enable_ssm_access` - (bool) - Whether to allow access to SSM. Defaults to false. 18 | - `enable_s3_access` - (bool) - Whether to allow access to S3. Defaults to false. 19 | - `s3_bucket_component_name` - (string) - The name of the component to use for the S3 bucket. Defaults to `s3-bucket/github-action-artifacts`. 20 | - `s3_bucket_environment_name` - (string) - The name of the environment to use for the S3 bucket. Defaults to the environment of the current module. 21 | - `s3_bucket_tenant_name` - (string) - The name of the tenant to use for the S3 bucket. Defaults to the tenant of the current module. 22 | - `s3_bucket_stage_name` - (string) - The name of the stage to use for the S3 bucket. Defaults to the stage of the current module. 23 | - `enable_lambda_update` - (bool) - Whether to allow access to update lambda functions. Defaults to false. 24 | EOT 25 | } 26 | 27 | locals { 28 | lambda_cicd_policy_enabled = contains(var.iam_policies, "lambda-cicd") 29 | lambda_cicd_policy = local.lambda_cicd_policy_enabled ? one(data.aws_iam_policy_document.lambda_cicd_policy.*.json) : null 30 | 31 | lambda_bucket_arn = try(module.s3_artifacts_bucket[0].outputs.bucket_arn, null) 32 | } 33 | 34 | module "s3_artifacts_bucket" { 35 | count = lookup(var.lambda_cicd_policy_configuration, "enable_s3_access", false) ? 1 : 0 36 | 37 | source = "cloudposse/stack-config/yaml//modules/remote-state" 38 | version = "1.5.0" 39 | 40 | component = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_component_name", "s3-bucket/github-action-artifacts") 41 | environment = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_environment_name", module.this.environment) 42 | tenant = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_tenant_name", module.this.tenant) 43 | stage = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_stage_name", module.this.stage) 44 | 45 | context = module.this.context 46 | } 47 | 48 | data "aws_iam_policy_document" "lambda_cicd_policy" { 49 | count = local.lambda_cicd_policy_enabled ? 1 : 0 50 | 51 | dynamic "statement" { 52 | for_each = lookup(var.lambda_cicd_policy_configuration, "enable_kms_access", false) ? [1] : [] 53 | content { 54 | sid = "AllowKMSAccess" 55 | effect = "Allow" 56 | actions = [ 57 | "kms:DescribeKey", 58 | "kms:Encrypt", 59 | ] 60 | resources = [ 61 | "*" 62 | ] 63 | } 64 | } 65 | 66 | dynamic "statement" { 67 | for_each = lookup(var.lambda_cicd_policy_configuration, "enable_ssm_access", false) ? [1] : [] 68 | content { 69 | effect = "Allow" 70 | actions = [ 71 | "ssm:GetParameter", 72 | "ssm:GetParameters", 73 | "ssm:GetParametersByPath", 74 | "ssm:DescribeParameters", 75 | "ssm:PutParameter" 76 | ] 77 | resources = [ 78 | "arn:aws:ssm:*:*:parameter/lambda/*" 79 | ] 80 | } 81 | } 82 | 83 | dynamic "statement" { 84 | for_each = lookup(var.lambda_cicd_policy_configuration, "enable_s3_access", false) && local.lambda_bucket_arn != null ? [1] : [] 85 | content { 86 | effect = "Allow" 87 | actions = [ 88 | "s3:HeadObject", 89 | "s3:GetObject", 90 | "s3:PutObject", 91 | "s3:ListBucket", 92 | "s3:GetBucketLocation" 93 | ] 94 | resources = [ 95 | local.lambda_bucket_arn, 96 | ] 97 | } 98 | } 99 | 100 | dynamic "statement" { 101 | for_each = lookup(var.lambda_cicd_policy_configuration, "enable_lambda_update", false) ? [1] : [] 102 | content { 103 | effect = "Allow" 104 | actions = [ 105 | "lambda:UpdateFunctionCode", 106 | "lambda:UpdateFunctionConfiguration" 107 | ] 108 | resources = [ 109 | "*" 110 | ] 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/github-oidc-role/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | 4 | # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. 5 | profile = module.iam_roles.terraform_profile_name 6 | 7 | dynamic "assume_role" { 8 | # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. 9 | for_each = compact([module.iam_roles.terraform_role_arn]) 10 | content { 11 | role_arn = assume_role.value 12 | } 13 | } 14 | } 15 | 16 | module "iam_roles" { 17 | source = "../account-map/modules/iam-roles" 18 | context = module.this.context 19 | } 20 | -------------------------------------------------------------------------------- /tests/github-oidc-role/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | type = string 3 | description = "AWS Region" 4 | } 5 | 6 | variable "iam_policies" { 7 | type = list(string) 8 | description = "List of policies to attach to the IAM role, should be either an ARN of an AWS Managed Policy or a name of a custom policy e.g. `gitops`" 9 | default = [] 10 | } 11 | 12 | variable "iam_policy" { 13 | type = list(object({ 14 | policy_id = optional(string, null) 15 | version = optional(string, null) 16 | statements = list(object({ 17 | sid = optional(string, null) 18 | effect = optional(string, null) 19 | actions = optional(list(string), null) 20 | not_actions = optional(list(string), null) 21 | resources = optional(list(string), null) 22 | not_resources = optional(list(string), null) 23 | conditions = optional(list(object({ 24 | test = string 25 | variable = string 26 | values = list(string) 27 | })), []) 28 | principals = optional(list(object({ 29 | type = string 30 | identifiers = list(string) 31 | })), []) 32 | not_principals = optional(list(object({ 33 | type = string 34 | identifiers = list(string) 35 | })), []) 36 | })) 37 | })) 38 | description = <<-EOT 39 | IAM policy as list of Terraform objects, compatible with Terraform `aws_iam_policy_document` data source 40 | except that `source_policy_documents` and `override_policy_documents` are not included. 41 | Use inputs `iam_source_policy_documents` and `iam_override_policy_documents` for that. 42 | EOT 43 | default = [] 44 | nullable = false 45 | } 46 | 47 | 48 | variable "github_actions_allowed_repos" { 49 | type = list(string) 50 | description = <`, 17 | # with final values filled in. 18 | # 19 | # For example, when using defaults, `module.this.context.delimiter` 20 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 21 | # 22 | 23 | module "this" { 24 | source = "cloudposse/label/null" 25 | version = "0.25.0" # requires Terraform >= 0.13.0 26 | 27 | enabled = var.enabled 28 | namespace = var.namespace 29 | tenant = var.tenant 30 | environment = var.environment 31 | stage = var.stage 32 | name = var.name 33 | delimiter = var.delimiter 34 | attributes = var.attributes 35 | tags = var.tags 36 | additional_tag_map = var.additional_tag_map 37 | label_order = var.label_order 38 | regex_replace_chars = var.regex_replace_chars 39 | id_length_limit = var.id_length_limit 40 | label_key_case = var.label_key_case 41 | label_value_case = var.label_value_case 42 | descriptor_formats = var.descriptor_formats 43 | labels_as_tags = var.labels_as_tags 44 | 45 | context = var.context 46 | } 47 | 48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 49 | 50 | variable "context" { 51 | type = any 52 | default = { 53 | enabled = true 54 | namespace = null 55 | tenant = null 56 | environment = null 57 | stage = null 58 | name = null 59 | delimiter = null 60 | attributes = [] 61 | tags = {} 62 | additional_tag_map = {} 63 | regex_replace_chars = null 64 | label_order = [] 65 | id_length_limit = null 66 | label_key_case = null 67 | label_value_case = null 68 | descriptor_formats = {} 69 | # Note: we have to use [] instead of null for unset lists due to 70 | # https://github.com/hashicorp/terraform/issues/28137 71 | # which was not fixed until Terraform 1.0.0, 72 | # but we want the default to be all the labels in `label_order` 73 | # and we want users to be able to prevent all tag generation 74 | # by setting `labels_as_tags` to `[]`, so we need 75 | # a different sentinel to indicate "default" 76 | labels_as_tags = ["unset"] 77 | } 78 | description = <<-EOT 79 | Single object for setting entire context at once. 80 | See description of individual variables for details. 81 | Leave string and numeric variables as `null` to use default value. 82 | Individual variable settings (non-null) override settings in context object, 83 | except for attributes, tags, and additional_tag_map, which are merged. 84 | EOT 85 | 86 | validation { 87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 88 | error_message = "Allowed values: `lower`, `title`, `upper`." 89 | } 90 | 91 | validation { 92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 94 | } 95 | } 96 | 97 | variable "enabled" { 98 | type = bool 99 | default = null 100 | description = "Set to false to prevent the module from creating any resources" 101 | } 102 | 103 | variable "namespace" { 104 | type = string 105 | default = null 106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 107 | } 108 | 109 | variable "tenant" { 110 | type = string 111 | default = null 112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 113 | } 114 | 115 | variable "environment" { 116 | type = string 117 | default = null 118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 119 | } 120 | 121 | variable "stage" { 122 | type = string 123 | default = null 124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 125 | } 126 | 127 | variable "name" { 128 | type = string 129 | default = null 130 | description = <<-EOT 131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 132 | This is the only ID element not also included as a `tag`. 133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 134 | EOT 135 | } 136 | 137 | variable "delimiter" { 138 | type = string 139 | default = null 140 | description = <<-EOT 141 | Delimiter to be used between ID elements. 142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 143 | EOT 144 | } 145 | 146 | variable "attributes" { 147 | type = list(string) 148 | default = [] 149 | description = <<-EOT 150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 151 | in the order they appear in the list. New attributes are appended to the 152 | end of the list. The elements of the list are joined by the `delimiter` 153 | and treated as a single ID element. 154 | EOT 155 | } 156 | 157 | variable "labels_as_tags" { 158 | type = set(string) 159 | default = ["default"] 160 | description = <<-EOT 161 | Set of labels (ID elements) to include as tags in the `tags` output. 162 | Default is to include all labels. 163 | Tags with empty values will not be included in the `tags` output. 164 | Set to `[]` to suppress all generated tags. 165 | **Notes:** 166 | The value of the `name` tag, if included, will be the `id`, not the `name`. 167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 168 | changed in later chained modules. Attempts to change it will be silently ignored. 169 | EOT 170 | } 171 | 172 | variable "tags" { 173 | type = map(string) 174 | default = {} 175 | description = <<-EOT 176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 177 | Neither the tag keys nor the tag values will be modified by this module. 178 | EOT 179 | } 180 | 181 | variable "additional_tag_map" { 182 | type = map(string) 183 | default = {} 184 | description = <<-EOT 185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 186 | This is for some rare cases where resources want additional configuration of tags 187 | and therefore take a list of maps with tag key, value, and additional configuration. 188 | EOT 189 | } 190 | 191 | variable "label_order" { 192 | type = list(string) 193 | default = null 194 | description = <<-EOT 195 | The order in which the labels (ID elements) appear in the `id`. 196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 198 | EOT 199 | } 200 | 201 | variable "regex_replace_chars" { 202 | type = string 203 | default = null 204 | description = <<-EOT 205 | Terraform regular expression (regex) string. 206 | Characters matching the regex will be removed from the ID elements. 207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 208 | EOT 209 | } 210 | 211 | variable "id_length_limit" { 212 | type = number 213 | default = null 214 | description = <<-EOT 215 | Limit `id` to this many characters (minimum 6). 216 | Set to `0` for unlimited length. 217 | Set to `null` for keep the existing setting, which defaults to `0`. 218 | Does not affect `id_full`. 219 | EOT 220 | validation { 221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 223 | } 224 | } 225 | 226 | variable "label_key_case" { 227 | type = string 228 | default = null 229 | description = <<-EOT 230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 231 | Does not affect keys of tags passed in via the `tags` input. 232 | Possible values: `lower`, `title`, `upper`. 233 | Default value: `title`. 234 | EOT 235 | 236 | validation { 237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 238 | error_message = "Allowed values: `lower`, `title`, `upper`." 239 | } 240 | } 241 | 242 | variable "label_value_case" { 243 | type = string 244 | default = null 245 | description = <<-EOT 246 | Controls the letter case of ID elements (labels) as included in `id`, 247 | set as tag values, and output by this module individually. 248 | Does not affect values of tags passed in via the `tags` input. 249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 251 | Default value: `lower`. 252 | EOT 253 | 254 | validation { 255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 257 | } 258 | } 259 | 260 | variable "descriptor_formats" { 261 | type = any 262 | default = {} 263 | description = <<-EOT 264 | Describe additional descriptors to be output in the `descriptors` output map. 265 | Map of maps. Keys are names of descriptors. Values are maps of the form 266 | `{ 267 | format = string 268 | labels = list(string) 269 | }` 270 | (Type is `any` so the map values can later be enhanced to provide additional options.) 271 | `format` is a Terraform format string to be passed to the `format()` function. 272 | `labels` is a list of labels, in order, to pass to `format()` function. 273 | Label values will be normalized before being passed to `format()` so they will be 274 | identical to how they appear in `id`. 275 | Default is `{}` (`descriptors` output will be empty). 276 | EOT 277 | } 278 | 279 | #### End of copy of cloudposse/terraform-null-label/variables.tf 280 | -------------------------------------------------------------------------------- /tests/s3-bucket/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | enabled = module.this.enabled 3 | 4 | aws_partition = data.aws_partition.current.partition 5 | 6 | custom_policy_account_arns = [ 7 | for acct in var.custom_policy_account_names : 8 | format("arn:%s:iam::%s:root", local.aws_partition, module.account_map.outputs.full_account_map[acct]) 9 | ] 10 | 11 | bucket_policy = var.custom_policy_enabled ? data.aws_iam_policy_document.custom_policy[0].json : data.template_file.bucket_policy.rendered 12 | 13 | logging = var.logging != null ? { 14 | bucket_name = var.logging_bucket_name_rendering_enabled ? format(var.logging_bucket_name_rendering_template, var.namespace, var.tenant, var.environment, var.stage, var.logging.bucket_name) : var.logging.bucket_name 15 | prefix = var.logging_bucket_name_rendering_enabled ? format(var.logging_bucket_prefix_rendering_template, var.logging.prefix, var.name) : var.logging.prefix 16 | } : null 17 | } 18 | 19 | data "aws_partition" "current" {} 20 | 21 | data "template_file" "bucket_policy" { 22 | template = module.bucket_policy.json 23 | 24 | vars = { 25 | id = module.this.id 26 | } 27 | } 28 | 29 | module "bucket_policy" { 30 | source = "cloudposse/iam-policy/aws" 31 | version = "0.4.0" 32 | 33 | iam_policy_statements = var.iam_policy_statements 34 | 35 | context = module.this.context 36 | } 37 | 38 | module "s3_bucket" { 39 | source = "cloudposse/s3-bucket/aws" 40 | version = "3.1.1" 41 | 42 | bucket_name = var.bucket_name 43 | 44 | # Object access and permissions 45 | acl = var.acl 46 | grants = var.grants 47 | allow_encrypted_uploads_only = var.allow_encrypted_uploads_only 48 | allow_ssl_requests_only = var.allow_ssl_requests_only 49 | block_public_acls = var.block_public_acls 50 | block_public_policy = var.block_public_policy 51 | ignore_public_acls = var.ignore_public_acls 52 | restrict_public_buckets = var.restrict_public_buckets 53 | logging = local.logging 54 | source_policy_documents = [local.bucket_policy] 55 | privileged_principal_actions = var.privileged_principal_actions 56 | privileged_principal_arns = var.privileged_principal_arns 57 | s3_object_ownership = var.s3_object_ownership 58 | 59 | # Static website configuration 60 | cors_configuration = var.cors_configuration 61 | 62 | # Version 2.0.0 introduced a breaking change for `var.website_inputs`. 63 | # If you are using website_inputs, do not upgrade to v2.x yet. 64 | # See https://github.com/cloudposse/terraform-aws-s3-bucket/releases/tag/2.0.0 65 | # website_inputs = var.website_inputs 66 | 67 | # Bucket feature flags 68 | transfer_acceleration_enabled = var.transfer_acceleration_enabled 69 | versioning_enabled = var.versioning_enabled 70 | force_destroy = var.force_destroy 71 | object_lock_configuration = var.object_lock_configuration 72 | 73 | # Object lifecycle rules 74 | lifecycle_configuration_rules = var.lifecycle_configuration_rules 75 | 76 | # Object encryption 77 | sse_algorithm = var.sse_algorithm 78 | kms_master_key_arn = var.kms_master_key_arn 79 | bucket_key_enabled = var.bucket_key_enabled 80 | 81 | # Object replication 82 | s3_replication_enabled = var.s3_replication_enabled 83 | s3_replica_bucket_arn = var.s3_replica_bucket_arn 84 | s3_replication_rules = var.s3_replication_rules 85 | s3_replication_source_roles = var.s3_replication_source_roles 86 | 87 | # IAM user with permissions to access the s3 bucket 88 | user_enabled = var.user_enabled 89 | allowed_bucket_actions = var.allowed_bucket_actions 90 | 91 | context = module.this.context 92 | } 93 | 94 | data "aws_iam_policy_document" "custom_policy" { 95 | count = local.enabled && var.custom_policy_enabled ? 1 : 0 96 | 97 | statement { 98 | actions = var.custom_policy_actions 99 | 100 | resources = [ 101 | format("arn:%s:s3:::%s", local.aws_partition, module.this.id), 102 | format("arn:%s:s3:::%s/*", local.aws_partition, module.this.id) 103 | ] 104 | principals { 105 | identifiers = length(local.custom_policy_account_arns) > 0 ? local.custom_policy_account_arns : ["*"] 106 | type = "AWS" 107 | } 108 | 109 | effect = "Allow" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/s3-bucket/outputs.tf: -------------------------------------------------------------------------------- 1 | output "bucket_domain_name" { 2 | value = module.s3_bucket.bucket_domain_name 3 | description = "Bucket domain name" 4 | } 5 | 6 | output "bucket_regional_domain_name" { 7 | value = module.s3_bucket.bucket_regional_domain_name 8 | description = "Bucket region-specific domain name" 9 | } 10 | 11 | output "bucket_id" { 12 | value = module.s3_bucket.bucket_id 13 | description = "Bucket ID" 14 | } 15 | 16 | output "bucket_arn" { 17 | value = module.s3_bucket.bucket_arn 18 | description = "Bucket ARN" 19 | } 20 | 21 | output "bucket_region" { 22 | value = module.s3_bucket.bucket_region 23 | description = "Bucket region" 24 | } 25 | -------------------------------------------------------------------------------- /tests/s3-bucket/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | 4 | # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. 5 | profile = module.iam_roles.terraform_profile_name 6 | 7 | dynamic "assume_role" { 8 | # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. 9 | for_each = compact([module.iam_roles.terraform_role_arn]) 10 | content { 11 | role_arn = assume_role.value 12 | } 13 | } 14 | } 15 | 16 | module "iam_roles" { 17 | source = "../account-map/modules/iam-roles" 18 | context = module.this.context 19 | } 20 | -------------------------------------------------------------------------------- /tests/s3-bucket/remote-state.tf: -------------------------------------------------------------------------------- 1 | module "account_map" { 2 | source = "cloudposse/stack-config/yaml//modules/remote-state" 3 | version = "1.5.0" 4 | 5 | component = "account-map" 6 | environment = var.account_map_environment_name 7 | stage = var.account_map_stage_name 8 | tenant = var.account_map_tenant_name 9 | 10 | context = module.this.context 11 | } 12 | -------------------------------------------------------------------------------- /tests/s3-bucket/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | type = string 3 | description = "AWS Region" 4 | } 5 | 6 | variable "account_map_environment_name" { 7 | type = string 8 | description = "The name of the environment where `account_map` is provisioned" 9 | default = "gbl" 10 | } 11 | 12 | variable "account_map_stage_name" { 13 | type = string 14 | description = "The name of the stage where `account_map` is provisioned" 15 | default = "root" 16 | } 17 | 18 | variable "account_map_tenant_name" { 19 | type = string 20 | description = <<-EOT 21 | The name of the tenant where `account_map` is provisioned. 22 | 23 | If the `tenant` label is not used, leave this as `null`. 24 | EOT 25 | default = null 26 | } 27 | 28 | variable "acl" { 29 | type = string 30 | default = "private" 31 | description = <<-EOT 32 | The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. 33 | We recommend `private` to avoid exposing sensitive information. Conflicts with `grants`. 34 | EOT 35 | } 36 | 37 | variable "grants" { 38 | type = list(object({ 39 | id = string 40 | type = string 41 | permissions = list(string) 42 | uri = string 43 | })) 44 | default = [] 45 | 46 | description = <<-EOT 47 | A list of policy grants for the bucket, taking a list of permissions. 48 | Conflicts with `acl`. Set `acl` to `null` to use this. 49 | EOT 50 | } 51 | 52 | variable "source_policy_documents" { 53 | type = list(string) 54 | default = [] 55 | description = <<-EOT 56 | List of IAM policy documents that are merged together into the exported document. 57 | Statements defined in source_policy_documents or source_json must have unique SIDs. 58 | Statement having SIDs that match policy SIDs generated by this module will override them. 59 | EOT 60 | } 61 | 62 | variable "force_destroy" { 63 | type = bool 64 | default = false 65 | description = <<-EOT 66 | When `true`, permits a non-empty S3 bucket to be deleted by first deleting all objects in the bucket. 67 | THESE OBJECTS ARE NOT RECOVERABLE even if they were versioned and stored in Glacier. 68 | EOT 69 | } 70 | 71 | variable "versioning_enabled" { 72 | type = bool 73 | default = true 74 | description = "A state of versioning. Versioning is a means of keeping multiple variants of an object in the same bucket" 75 | } 76 | 77 | variable "logging_bucket_name_rendering_enabled" { 78 | type = bool 79 | default = false 80 | description = "Whether to render the logging bucket name, prepending context" 81 | } 82 | 83 | variable "logging_bucket_name_rendering_template" { 84 | type = string 85 | default = "%s-%s-%s-%s-%s" 86 | description = <<-EOT 87 | The template for the template used to render Bucket Name for the Logging bucket. 88 | Default is appropriate when using `tenant` and default label order with `null-label`. 89 | Use `"%s-%s-%s-%%s"` when not using `tenant`. 90 | EOT 91 | } 92 | 93 | variable "logging_bucket_prefix_rendering_template" { 94 | type = string 95 | default = "%s/%s/" 96 | description = "The template for the template used to render Bucket Prefix for the Logging bucket, uses the format `var.logging.prefix`/`var.name`" 97 | } 98 | 99 | variable "logging" { 100 | type = object({ 101 | bucket_name = string 102 | prefix = string 103 | }) 104 | default = null 105 | description = "Bucket access logging configuration." 106 | } 107 | 108 | variable "sse_algorithm" { 109 | type = string 110 | default = "AES256" 111 | description = "The server-side encryption algorithm to use. Valid values are `AES256` and `aws:kms`" 112 | } 113 | 114 | variable "kms_master_key_arn" { 115 | type = string 116 | default = "" 117 | description = "The AWS KMS master key ARN used for the `SSE-KMS` encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default aws/s3 AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms`" 118 | } 119 | 120 | variable "user_enabled" { 121 | type = bool 122 | default = false 123 | description = "Set to `true` to create an IAM user with permission to access the bucket" 124 | } 125 | 126 | variable "allowed_bucket_actions" { 127 | type = list(string) 128 | default = ["s3:PutObject", "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation", "s3:AbortMultipartUpload"] 129 | description = "List of actions the user is permitted to perform on the S3 bucket" 130 | } 131 | 132 | variable "allow_encrypted_uploads_only" { 133 | type = bool 134 | default = false 135 | description = "Set to `true` to prevent uploads of unencrypted objects to S3 bucket" 136 | } 137 | 138 | variable "allow_ssl_requests_only" { 139 | type = bool 140 | default = false 141 | description = "Set to `true` to require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests" 142 | } 143 | 144 | /* 145 | Schema for lifecycle_configuration_rules 146 | { 147 | enabled = true # bool 148 | id = string 149 | abort_incomplete_multipart_upload_days = null # number 150 | filter_and = { 151 | object_size_greater_than = null # integer >= 0 152 | object_size_less_than = null # integer >= 1 153 | prefix = null # string 154 | tags = {} # map(string) 155 | } 156 | expiration = { 157 | date = null # string, RFC3339 time format, GMT 158 | days = null # integer > 0 159 | expired_object_delete_marker = null # bool 160 | } 161 | noncurrent_version_expiration = { 162 | newer_noncurrent_versions = null # integer > 0 163 | noncurrent_days = null # integer >= 0 164 | } 165 | transition = [{ 166 | date = null # string, RFC3339 time format, GMT 167 | days = null # integer >= 0 168 | storage_class = null # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. 169 | }] 170 | noncurrent_version_transition = [{ 171 | newer_noncurrent_versions = null # integer >= 0 172 | noncurrent_days = null # integer >= 0 173 | storage_class = null # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. 174 | }] 175 | } 176 | We only partly specify the object to allow for compatible future extension. 177 | */ 178 | 179 | variable "lifecycle_configuration_rules" { 180 | type = list(object({ 181 | enabled = bool 182 | id = string 183 | 184 | abort_incomplete_multipart_upload_days = number 185 | 186 | # `filter_and` is the `and` configuration block inside the `filter` configuration. 187 | # This is the only place you should specify a prefix. 188 | filter_and = any 189 | expiration = any 190 | transition = list(any) 191 | 192 | noncurrent_version_expiration = any 193 | noncurrent_version_transition = list(any) 194 | })) 195 | default = [] 196 | description = "A list of lifecycle V2 rules" 197 | } 198 | 199 | variable "cors_configuration" { 200 | type = list(object({ 201 | allowed_headers = list(string) 202 | allowed_methods = list(string) 203 | allowed_origins = list(string) 204 | expose_headers = list(string) 205 | max_age_seconds = number 206 | })) 207 | default = null 208 | 209 | description = "Specifies the allowed headers, methods, origins and exposed headers when using CORS on this bucket" 210 | } 211 | 212 | variable "block_public_acls" { 213 | type = bool 214 | default = true 215 | description = "Set to `false` to disable the blocking of new public access lists on the bucket" 216 | } 217 | 218 | variable "block_public_policy" { 219 | type = bool 220 | default = true 221 | description = "Set to `false` to disable the blocking of new public policies on the bucket" 222 | } 223 | 224 | variable "ignore_public_acls" { 225 | type = bool 226 | default = true 227 | description = "Set to `false` to disable the ignoring of public access lists on the bucket" 228 | } 229 | 230 | variable "restrict_public_buckets" { 231 | type = bool 232 | default = true 233 | description = "Set to `false` to disable the restricting of making the bucket public" 234 | } 235 | 236 | variable "s3_replication_enabled" { 237 | type = bool 238 | default = false 239 | description = "Set this to true and specify `s3_replication_rules` to enable replication. `versioning_enabled` must also be `true`." 240 | } 241 | 242 | variable "s3_replica_bucket_arn" { 243 | type = string 244 | default = "" 245 | description = <<-EOT 246 | A single S3 bucket ARN to use for all replication rules. 247 | Note: The destination bucket can be specified in the replication rule itself 248 | (which allows for multiple destinations), in which case it will take precedence over this variable. 249 | EOT 250 | } 251 | 252 | variable "s3_replication_rules" { 253 | # type = list(object({ 254 | # id = string 255 | # priority = number 256 | # prefix = string 257 | # status = string 258 | # delete_marker_replication_status = string 259 | # # destination_bucket is specified here rather than inside the destination object 260 | # # to make it easier to work with the Terraform type system and create a list of consistent type. 261 | # destination_bucket = string # destination bucket ARN, overrides s3_replica_bucket_arn 262 | # 263 | # destination = object({ 264 | # storage_class = string 265 | # replica_kms_key_id = string 266 | # access_control_translation = object({ 267 | # owner = string 268 | # }) 269 | # account_id = string 270 | # metrics = object({ 271 | # status = string 272 | # }) 273 | # }) 274 | # source_selection_criteria = object({ 275 | # sse_kms_encrypted_objects = object({ 276 | # enabled = bool 277 | # }) 278 | # }) 279 | # # filter.prefix overrides top level prefix 280 | # filter = object({ 281 | # prefix = string 282 | # tags = map(string) 283 | # }) 284 | # })) 285 | 286 | type = list(any) 287 | default = null 288 | description = "Specifies the replication rules for S3 bucket replication if enabled. You must also set s3_replication_enabled to true." 289 | } 290 | 291 | variable "s3_replication_source_roles" { 292 | type = list(string) 293 | default = [] 294 | description = "Cross-account IAM Role ARNs that will be allowed to perform S3 replication to this bucket (for replication within the same AWS account, it's not necessary to adjust the bucket policy)." 295 | } 296 | 297 | variable "bucket_name" { 298 | type = string 299 | default = "" 300 | description = "Bucket name. If provided, the bucket will be created with this name instead of generating the name from the context" 301 | } 302 | 303 | variable "object_lock_configuration" { 304 | type = object({ 305 | mode = string # Valid values are GOVERNANCE and COMPLIANCE. 306 | days = number 307 | years = number 308 | }) 309 | default = null 310 | description = "A configuration for S3 object locking. With S3 Object Lock, you can store objects using a `write once, read many` (WORM) model. Object Lock can help prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely." 311 | } 312 | 313 | variable "website_inputs" { 314 | type = list(object({ 315 | index_document = string 316 | error_document = string 317 | redirect_all_requests_to = string 318 | routing_rules = string 319 | })) 320 | default = null 321 | description = "Specifies the static website hosting configuration object." 322 | validation { 323 | condition = var.website_inputs == null 324 | error_message = "The \"cloudposse/s3-bucket/aws\" module v2.0.0 introduced a breaking change for website_inputs and will be fixed with future updates." 325 | } 326 | } 327 | 328 | # Need input to be a list to fix https://github.com/cloudposse/terraform-aws-s3-bucket/issues/102 329 | variable "privileged_principal_arns" { 330 | # type = map(list(string)) 331 | # default = {} 332 | type = list(map(list(string))) 333 | default = [] 334 | 335 | description = <<-EOT 336 | List of maps. Each map has one key, an IAM Principal ARN, whose associated value is 337 | a list of S3 path prefixes to grant `privileged_principal_actions` permissions for that principal, 338 | in addition to the bucket itself, which is automatically included. Prefixes should not begin with '/'. 339 | EOT 340 | } 341 | 342 | variable "privileged_principal_actions" { 343 | type = list(string) 344 | default = [] 345 | description = "List of actions to permit `privileged_principal_arns` to perform on bucket and bucket prefixes (see `privileged_principal_arns`)" 346 | } 347 | 348 | variable "transfer_acceleration_enabled" { 349 | type = bool 350 | default = false 351 | description = "Set this to true to enable S3 Transfer Acceleration for the bucket." 352 | } 353 | 354 | variable "s3_object_ownership" { 355 | type = string 356 | default = "ObjectWriter" 357 | description = "Specifies the S3 object ownership control. Valid values are `ObjectWriter`, `BucketOwnerPreferred`, and 'BucketOwnerEnforced'." 358 | } 359 | 360 | variable "bucket_key_enabled" { 361 | type = bool 362 | default = false 363 | description = <<-EOT 364 | Set this to true to use Amazon S3 Bucket Keys for SSE-KMS, which reduce the cost of AWS KMS requests. 365 | For more information, see: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-key.html 366 | EOT 367 | } 368 | 369 | variable "custom_policy_actions" { 370 | description = "List of S3 Actions for the custom policy" 371 | type = list(string) 372 | default = [] 373 | } 374 | 375 | variable "custom_policy_account_names" { 376 | description = "List of accounts names to assign as principals for the s3 bucket custom policy" 377 | type = list(string) 378 | default = [] 379 | } 380 | 381 | variable "custom_policy_enabled" { 382 | description = "Whether to enable or disable the custom policy. If enabled, the default policy will be ignored" 383 | type = bool 384 | default = false 385 | } 386 | 387 | variable "iam_policy_statements" { 388 | type = any 389 | description = "Map of IAM policy statements to use in the bucket policy." 390 | default = {} 391 | } 392 | -------------------------------------------------------------------------------- /tests/s3-bucket/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 4.0" 8 | } 9 | template = { 10 | source = "cloudposse/template" 11 | version = ">= 2.2.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/small/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.7.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.0" 8 | } 9 | } 10 | } 11 | 12 | ############################ 13 | # Providers 14 | ############################ 15 | provider "aws" { 16 | region = var.aws_region 17 | } 18 | 19 | ############################ 20 | # Variables (including a sensitive one) 21 | ############################ 22 | variable "aws_region" { 23 | type = string 24 | description = "AWS region to deploy into" 25 | default = "us-east-1" 26 | } 27 | 28 | variable "project" { 29 | type = string 30 | description = "Project name used as a prefix for resources" 31 | } 32 | 33 | variable "db_password" { 34 | type = string 35 | description = "Database password (kept secret)" 36 | sensitive = true 37 | } 38 | 39 | ############################ 40 | # Data source 41 | ############################ 42 | 43 | data "aws_ssm_parameter" "password" { 44 | name = "secret" 45 | } 46 | 47 | ############################ 48 | # Module call 49 | ############################ 50 | module "vpc" { 51 | source = "terraform-aws-modules/vpc/aws" 52 | version = ">= 5.0.0" 53 | 54 | name = "${var.project}-vpc" 55 | cidr = "10.0.0.0/16" 56 | } 57 | 58 | ############################ 59 | # Resource example 60 | ############################ 61 | resource "aws_s3_bucket" "artifact" { 62 | bucket = "${var.project}-artifact-bucket" 63 | 64 | tags = { 65 | Project = var.project 66 | } 67 | } 68 | 69 | ############################ 70 | # Outputs 71 | ############################ 72 | output "vpc_id" { 73 | description = "ID of the VPC created by the module" 74 | value = module.vpc.vpc_id 75 | } 76 | 77 | output "artifact_bucket_id" { 78 | description = "ID of the S3 bucket for artifacts" 79 | value = aws_s3_bucket.artifact.id 80 | } 81 | 82 | output "sensitive_variable_example" { 83 | description = "Demonstrates a sensitive output (will be redacted on CLI)" 84 | value = var.db_password 85 | sensitive = true 86 | } 87 | 88 | -------------------------------------------------------------------------------- /tests/vpc/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "The name of the VPC" 3 | type = string 4 | default = "vpc" 5 | } 6 | 7 | resource "random_id" "id" { 8 | byte_length = 8 9 | } 10 | 11 | locals { 12 | mock_vpc_id = "${var.name}-${random_id.id.hex}" 13 | } 14 | 15 | output "vpc_id" { 16 | value = local.mock_vpc_id 17 | } 18 | -------------------------------------------------------------------------------- /tests/vpc/providers.tf: -------------------------------------------------------------------------------- 1 | provider "random" { 2 | # Configuration options for the provider (if any) 3 | } 4 | --------------------------------------------------------------------------------