├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── SUPPORT.md ├── dependabot.yml └── workflows │ ├── actionlint.yml │ ├── test.yml │ └── update-helm-charts-index.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Chart.yaml ├── LICENSE ├── META.d ├── _summary.yaml └── links.yaml ├── Makefile ├── README.md ├── crds └── app.terraform.io_workspaces_crd.yaml ├── docs └── workspace-sync.html.md ├── example ├── configmap.yaml ├── greetings │ ├── Dockerfile │ ├── application.yaml │ └── message.sh ├── workspace_git.yaml ├── workspace_random.yml ├── workspace_registry.yaml ├── workspace_remote_agent.yaml └── workspace_vcs.yaml ├── templates ├── NOTES.txt ├── _helpers.tpl ├── sync-workspace-deployment.yaml ├── sync-workspace-role.yaml ├── sync-workspace-rolebinding.yaml ├── sync-workspace-secret.yaml ├── sync-workspace-serviceaccount.yaml └── tests │ ├── test-configmap.yaml │ ├── test-runner.yaml │ └── test-workspace.yaml ├── test ├── acceptance │ ├── _helpers.bash │ └── server.bats ├── docker │ └── Test.dockerfile ├── module │ └── random.tf └── unit │ ├── _helpers.bash │ ├── sync-workspace-deployment.bats │ ├── sync-workspace-role.bats │ ├── sync-workspace-rolebinding.bats │ ├── sync-workspace-serviceaccount.bats │ ├── test-configmap.bats │ ├── test-runner.bats │ └── test-workspace.bats ├── values.dev.yaml └── values.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/tf-eco-hybrid-cloud 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: "If something isn't working as expected \U0001F914." 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 18 | 19 | 20 | 21 | ### Community Note 22 | 23 | * Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request 24 | * Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request 25 | * If you are interested in working on this issue or have submitted a pull request, please leave a comment 26 | 27 | 28 | 29 | ### terraform-k8s, Helm, & Kubernetes Version 30 | 31 | 33 | 34 | ### Affected Resource(s) 35 | 36 | 37 | 38 | ### Helm Values 39 | 40 | 41 | 42 | ```yaml 43 | # Copy-paste your Helm values.yaml here. 44 | ``` 45 | 46 | ### Debug Output 47 | 48 | 51 | 52 | ### Expected Behavior 53 | 54 | 55 | 56 | ### Actual Behavior 57 | 58 | 59 | 60 | ### Steps to Reproduce 61 | 62 | 63 | 64 | ### Important Factoids 65 | 66 | 67 | 68 | ### References 69 | 70 | 75 | 76 | * #0000 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: "I have a suggestion (and might want to implement myself \U0001F642)!" 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Community Note 13 | 14 | * Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request 15 | * Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request 16 | * If you are interested in working on this issue or have submitted a pull request, please leave a comment 17 | 18 | 19 | 20 | ### Description 21 | 22 | 23 | 24 | ### Potential Helm Configuration 25 | 26 | 27 | 28 | ```yaml 29 | # If you have a general idea of the extension to make 30 | # to the chart, copy-paste it here. 31 | ``` 32 | 33 | ### References 34 | 35 | 40 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Terraform is a mature project with a growing community. There are active, dedicated people willing to help you through various mediums. 4 | 5 | Take a look at those mediums listed at https://www.terraform.io/community.html -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | # If the repository is public, be sure to change to GitHub hosted runners 2 | name: Lint GitHub Actions Workflows 3 | on: 4 | pull_request: 5 | paths: 6 | - .github/workflows/** 7 | permissions: 8 | contents: read 9 | jobs: 10 | actionlint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 14 | - name: "Check workflow files" 15 | uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: hashicorp/terraform-helm/test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | unit: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: docker.mirror.hashicorp.services/hashicorpdev/terraform-helm-test:0.1.0 15 | steps: 16 | - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 17 | - name: Run Unit Tests 18 | run: bats ./test/unit 19 | -------------------------------------------------------------------------------- /.github/workflows/update-helm-charts-index.yml: -------------------------------------------------------------------------------- 1 | name: update-helm-charts-index 2 | on: 3 | push: 4 | tags: 5 | - "v.*" 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | update-helm-charts-index: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 15 | 16 | - name: verify Chart version matches tag version 17 | env: 18 | ref_name: ${{ github.ref_name }} 19 | run: |- 20 | chart_tag="$(yq '.version' Chart.yaml)" 21 | git_tag="${ref_name#v}" 22 | if [ "${git_tag}" != "${chart_tag}" ]; then 23 | message="chart version (${chart_tag}) did not match git version (${git_tag})" 24 | echo "::error title=Version Mismatch::${message}" 25 | echo "${message}" 26 | exit 1 27 | fi 28 | 29 | - name: update helm-charts index 30 | env: 31 | GH_TOKEN: ${{ secrets.HELM_CHARTS_GITHUB_TOKEN }} 32 | run: |- 33 | ./bin/gh workflow run publish-charts.yml \ 34 | --repo hashicorp/helm-charts \ 35 | --ref main \ 36 | -f SOURCE_TAG="${{ github.ref_name }}" \ 37 | -f SOURCE_REPO="${{ github.repository }}" 38 | 39 | - name: send slack status 40 | if: failure() 41 | uses: hashicorp/actions-slack-status@v1 42 | with: 43 | status: "failure" 44 | slack-webhook-url: ${{ secrets.SLACK_WEBHOOK }} 45 | failure-message: "Failed to trigger an update to the helm charts index." 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.2 (November 22, 2022) 2 | 3 | * Update operator image to `v1.1.2` 4 | 5 | ## 1.1.1 (July 14, 2022) 6 | 7 | * Update operator image to `v1.1.1` 8 | * Update CRD schema to `apiextensions.k8s.io/v1` 9 | * Add new RBAC permissions `create`, `get`, `update` for API group `coordination.k8s.io`, resource `leases` 10 | 11 | ## 1.1.0 (Sep 10, 2021) 12 | 13 | * Update terraform-k8s to v1.1.0 14 | 15 | ## 1.0.0 (Feb 19, 2021) 16 | 17 | * Update terraform-k8s to v1.0.0 18 | 19 | ## 0.2.1 beta (Jan 06, 2021) 20 | 21 | * Update terraform-k8s to v0.2.1-beta 22 | 23 | ## 0.1.6 alpha (Sep 01, 2020) 24 | 25 | * New option for setting a custom CA cert in the operator image 26 | 27 | ## 0.1.5-alpha (May 20, 2020) 28 | 29 | * Update Helm chart to support Terraform Enterprise 30 | * Update terraform-k8s to v0.1.5-alpha 31 | 32 | ## 0.1.4-alpha (May 14, 2020) 33 | 34 | * Update Helm chart to pass TF_VERSION override 35 | * Update terraform-k8s to v0.1.4-alpha 36 | 37 | ## 0.1.3-alpha 38 | 39 | * Update terraform-k8s to v0.1.3-alpha 40 | 41 | ## 0.1.2-alpha 42 | 43 | * Update terraform-k8s to v0.1.2-alpha 44 | 45 | ## 0.1.0-alpha 46 | 47 | Initial release 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Rebasing contributions against master 4 | 5 | PRs in this repo are merged using the [`rebase`](https://git-scm.com/docs/git-rebase) method. This keeps 6 | the git history clean by adding the PR commits to the most recent end of the commit history. It also has 7 | the benefit of keeping all the relevant commits for a given PR together, rather than spread throughout the 8 | git history based on when the commits were first created. 9 | 10 | If the changes in your PR do not conflict with any of the existing code in the project, then Github supports 11 | automatic rebasing when the PR is accepted into the code. However, if there are conflicts (there will be 12 | a warning on the PR that reads "This branch cannot be rebased due to conflicts"), you will need to manually 13 | rebase the branch on master, fixing any conflicts along the way before the code can be merged. 14 | 15 | ## Testing 16 | 17 | The Helm chart ships with both unit and acceptance tests. 18 | 19 | The unit tests don't require any active Kubernetes cluster and complete 20 | very quickly. These should be used for fast feedback during development. 21 | The acceptance tests require a Kubernetes cluster with a configured `kubectl`. 22 | 23 | ### Prequisites 24 | * [Bats](https://github.com/bats-core/bats-core) 25 | ```bash 26 | brew install bats-core 27 | ``` 28 | * [yq](https://pypi.org/project/yq/) 29 | ```bash 30 | brew install python-yq 31 | ``` 32 | * [helm](https://helm.sh) 33 | ```bash 34 | brew install kubernetes-helm 35 | ``` 36 | 37 | ### Running The Tests 38 | To run the unit tests: 39 | 40 | bats ./test/unit 41 | 42 | To run the acceptance tests: 43 | 44 | bats ./test/acceptance 45 | 46 | You can edit two parameters as part of the acceptance tests. 47 | 48 | * `TF_CLI_CONFIG_FILE` environment variable. This is the file path to the Terraform 49 | Cloud credentials you want to use. By default, it will use `${HOME}/.terraformrc`. 50 | 51 | * `test/acceptance/values.yaml` with `test.organization` defined. By default, it will 52 | use the organization value in the top-level `values.yaml` of the chart. 53 | 54 | If the acceptance tests fail, deployed resources in the Kubernetes cluster 55 | may not be properly cleaned up. We recommend recycling the Kubernetes cluster to 56 | start from a clean slate. 57 | 58 | ### Writing Unit Tests 59 | 60 | Changes to the Helm chart should be accompanied by appropriate unit tests. 61 | 62 | #### Formatting 63 | 64 | - Put tests in the test file in the same order as the variables appear in the `values.yaml`. 65 | - Start tests for a chart value with a header that says what is being tested, like this: 66 | ``` 67 | #-------------------------------------------------------------------- 68 | # annotations 69 | ``` 70 | 71 | - Name the test based on what it's testing in the following format (this will be its first line): 72 | ``` 73 | @test "
: " { 74 | ``` 75 | 76 | When adding tests to an existing file, the first section will be the same as the other tests in the file. 77 | 78 | #### Test Details 79 | 80 | [Bats](https://github.com/bats-core/bats-core) provides a way to run commands in a shell and inspect the output in an automated way. 81 | In all of the tests in this repo, the base command being run is [helm template](https://docs.helm.sh/helm/#helm-template) which turns the templated files into straight yaml output. 82 | In this way, we're able to test that the various conditionals in the templates render as we would expect. 83 | 84 | Each test defines the files that should be rendered using the `-x` flag, then it might adjust chart values by adding `--set` flags as well. 85 | The output from this `helm template` command is then piped to [yq](https://pypi.org/project/yq/). 86 | `yq` allows us to pull out just the information we're interested in, either by referencing its position in the yaml file directly or giving information about it (like its length). 87 | The `-r` flag can be used with `yq` to return a raw string instead of a quoted one which is especially useful when looking for an exact match. 88 | 89 | The test passes or fails based on the conditional at the end that is in square brackets, which is a comparison of our expected value and the output of `helm template` piped to `yq`. 90 | 91 | The `| tee /dev/stderr ` pieces direct any terminal output of the `helm template` and `yq` commands to stderr so that it doesn't interfere with `bats` 92 | -------------------------------------------------------------------------------- /Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: terraform 3 | version: 1.1.2 4 | description: Install and configure Terraform Cloud Operator on Kubernetes. 5 | home: https://www.terraform.io 6 | sources: 7 | - https://github.com/hashicorp/terraform 8 | - https://github.com/hashicorp/terraform-helm 9 | - https://github.com/hashicorp/terraform-k8s 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 HashiCorp, Inc. 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. “Contributor” 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. “Contributor Version” 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor’s Contribution. 16 | 17 | 1.3. “Contribution” 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. “Covered Software” 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. “Incompatible With Secondary Licenses” 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of version 35 | 1.1 or earlier of the License, but not also under the terms of a 36 | Secondary License. 37 | 38 | 1.6. “Executable Form” 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. “Larger Work” 43 | 44 | means a work that combines Covered Software with other material, in a separate 45 | file or files, that is not Covered Software. 46 | 47 | 1.8. “License” 48 | 49 | means this document. 50 | 51 | 1.9. “Licensable” 52 | 53 | means having the right to grant, to the maximum extent possible, whether at the 54 | time of the initial grant or subsequently, any and all of the rights conveyed by 55 | this License. 56 | 57 | 1.10. “Modifications” 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, deletion 62 | from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. “Patent Claims” of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, process, 69 | and apparatus claims, in any patent Licensable by such Contributor that 70 | would be infringed, but for the grant of the License, by the making, 71 | using, selling, offering for sale, having made, import, or transfer of 72 | either its Contributions or its Contributor Version. 73 | 74 | 1.12. “Secondary License” 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. “Source Code Form” 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. “You” (or “Your”) 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, “You” includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, “control” means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or as 106 | part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its Contributions 110 | or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution become 115 | effective for each Contribution on the date the Contributor first distributes 116 | such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under this 121 | License. No additional rights or licenses will be implied from the distribution 122 | or licensing of Covered Software under this License. Notwithstanding Section 123 | 2.1(b) above, no patent license is granted by a Contributor: 124 | 125 | a. for any code that a Contributor has removed from Covered Software; or 126 | 127 | b. for infringements caused by: (i) Your and any other third party’s 128 | modifications of Covered Software, or (ii) the combination of its 129 | Contributions with other software (except as part of its Contributor 130 | Version); or 131 | 132 | c. under Patent Claims infringed by Covered Software in the absence of its 133 | Contributions. 134 | 135 | This License does not grant any rights in the trademarks, service marks, or 136 | logos of any Contributor (except as may be necessary to comply with the 137 | notice requirements in Section 3.4). 138 | 139 | 2.4. Subsequent Licenses 140 | 141 | No Contributor makes additional grants as a result of Your choice to 142 | distribute the Covered Software under a subsequent version of this License 143 | (see Section 10.2) or under the terms of a Secondary License (if permitted 144 | under the terms of Section 3.3). 145 | 146 | 2.5. Representation 147 | 148 | Each Contributor represents that the Contributor believes its Contributions 149 | are its original creation(s) or it has sufficient rights to grant the 150 | rights to its Contributions conveyed by this License. 151 | 152 | 2.6. Fair Use 153 | 154 | This License is not intended to limit any rights You have under applicable 155 | copyright doctrines of fair use, fair dealing, or other equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under the 169 | terms of this License. You must inform recipients that the Source Code Form 170 | of the Covered Software is governed by the terms of this License, and how 171 | they can obtain a copy of this License. You may not attempt to alter or 172 | restrict the recipients’ rights in the Source Code Form. 173 | 174 | 3.2. Distribution of Executable Form 175 | 176 | If You distribute Covered Software in Executable Form then: 177 | 178 | a. such Covered Software must also be made available in Source Code Form, 179 | as described in Section 3.1, and You must inform recipients of the 180 | Executable Form how they can obtain a copy of such Source Code Form by 181 | reasonable means in a timely manner, at a charge no more than the cost 182 | of distribution to the recipient; and 183 | 184 | b. You may distribute such Executable Form under the terms of this License, 185 | or sublicense it under different terms, provided that the license for 186 | the Executable Form does not attempt to limit or alter the recipients’ 187 | rights in the Source Code Form under this License. 188 | 189 | 3.3. Distribution of a Larger Work 190 | 191 | You may create and distribute a Larger Work under terms of Your choice, 192 | provided that You also comply with the requirements of this License for the 193 | Covered Software. If the Larger Work is a combination of Covered Software 194 | with a work governed by one or more Secondary Licenses, and the Covered 195 | Software is not Incompatible With Secondary Licenses, this License permits 196 | You to additionally distribute such Covered Software under the terms of 197 | such Secondary License(s), so that the recipient of the Larger Work may, at 198 | their option, further distribute the Covered Software under the terms of 199 | either this License or such Secondary License(s). 200 | 201 | 3.4. Notices 202 | 203 | You may not remove or alter the substance of any license notices (including 204 | copyright notices, patent notices, disclaimers of warranty, or limitations 205 | of liability) contained within the Source Code Form of the Covered 206 | Software, except that You may alter any license notices to the extent 207 | required to remedy known factual inaccuracies. 208 | 209 | 3.5. Application of Additional Terms 210 | 211 | You may choose to offer, and to charge a fee for, warranty, support, 212 | indemnity or liability obligations to one or more recipients of Covered 213 | Software. However, You may do so only on Your own behalf, and not on behalf 214 | of any Contributor. You must make it absolutely clear that any such 215 | warranty, support, indemnity, or liability obligation is offered by You 216 | alone, and You hereby agree to indemnify every Contributor for any 217 | liability incurred by such Contributor as a result of warranty, support, 218 | indemnity or liability terms You offer. You may include additional 219 | disclaimers of warranty and limitations of liability specific to any 220 | jurisdiction. 221 | 222 | 4. Inability to Comply Due to Statute or Regulation 223 | 224 | If it is impossible for You to comply with any of the terms of this License 225 | with respect to some or all of the Covered Software due to statute, judicial 226 | order, or regulation then You must: (a) comply with the terms of this License 227 | to the maximum extent possible; and (b) describe the limitations and the code 228 | they affect. Such description must be placed in a text file included with all 229 | distributions of the Covered Software under this License. Except to the 230 | extent prohibited by statute or regulation, such description must be 231 | sufficiently detailed for a recipient of ordinary skill to be able to 232 | understand it. 233 | 234 | 5. Termination 235 | 236 | 5.1. The rights granted under this License will terminate automatically if You 237 | fail to comply with any of its terms. However, if You become compliant, 238 | then the rights granted under this License from a particular Contributor 239 | are reinstated (a) provisionally, unless and until such Contributor 240 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 241 | if such Contributor fails to notify You of the non-compliance by some 242 | reasonable means prior to 60 days after You have come back into compliance. 243 | Moreover, Your grants from a particular Contributor are reinstated on an 244 | ongoing basis if such Contributor notifies You of the non-compliance by 245 | some reasonable means, this is the first time You have received notice of 246 | non-compliance with this License from such Contributor, and You become 247 | compliant prior to 30 days after Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, counter-claims, 251 | and cross-claims) alleging that a Contributor Version directly or 252 | indirectly infringes any patent, then the rights granted to You by any and 253 | all Contributors for the Covered Software under Section 2.1 of this License 254 | shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 257 | license agreements (excluding distributors and resellers) which have been 258 | validly granted by You or Your distributors under this License prior to 259 | termination shall survive termination. 260 | 261 | 6. Disclaimer of Warranty 262 | 263 | Covered Software is provided under this License on an “as is” basis, without 264 | warranty of any kind, either expressed, implied, or statutory, including, 265 | without limitation, warranties that the Covered Software is free of defects, 266 | merchantable, fit for a particular purpose or non-infringing. The entire 267 | risk as to the quality and performance of the Covered Software is with You. 268 | Should any Covered Software prove defective in any respect, You (not any 269 | Contributor) assume the cost of any necessary servicing, repair, or 270 | correction. This disclaimer of warranty constitutes an essential part of this 271 | License. No use of any Covered Software is authorized under this License 272 | except under this disclaimer. 273 | 274 | 7. Limitation of Liability 275 | 276 | Under no circumstances and under no legal theory, whether tort (including 277 | negligence), contract, or otherwise, shall any Contributor, or anyone who 278 | distributes Covered Software as permitted above, be liable to You for any 279 | direct, indirect, special, incidental, or consequential damages of any 280 | character including, without limitation, damages for lost profits, loss of 281 | goodwill, work stoppage, computer failure or malfunction, or any and all 282 | other commercial damages or losses, even if such party shall have been 283 | informed of the possibility of such damages. This limitation of liability 284 | shall not apply to liability for death or personal injury resulting from such 285 | party’s negligence to the extent applicable law prohibits such limitation. 286 | Some jurisdictions do not allow the exclusion or limitation of incidental or 287 | consequential damages, so this exclusion and limitation may not apply to You. 288 | 289 | 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the courts of 292 | a jurisdiction where the defendant maintains its principal place of business 293 | and such litigation shall be governed by laws of that jurisdiction, without 294 | reference to its conflict-of-law provisions. Nothing in this Section shall 295 | prevent a party’s ability to bring cross-claims or counter-claims. 296 | 297 | 9. Miscellaneous 298 | 299 | This License represents the complete agreement concerning the subject matter 300 | hereof. If any provision of this License is held to be unenforceable, such 301 | provision shall be reformed only to the extent necessary to make it 302 | enforceable. Any law or regulation which provides that the language of a 303 | contract shall be construed against the drafter shall not be used to construe 304 | this License against a Contributor. 305 | 306 | 307 | 10. Versions of the License 308 | 309 | 10.1. New Versions 310 | 311 | Mozilla Foundation is the license steward. Except as provided in Section 312 | 10.3, no one other than the license steward has the right to modify or 313 | publish new versions of this License. Each version will be given a 314 | distinguishing version number. 315 | 316 | 10.2. Effect of New Versions 317 | 318 | You may distribute the Covered Software under the terms of the version of 319 | the License under which You originally received the Covered Software, or 320 | under the terms of any subsequent version published by the license 321 | steward. 322 | 323 | 10.3. Modified Versions 324 | 325 | If you create software not governed by this License, and you want to 326 | create a new license for such software, you may create and use a modified 327 | version of this License if you rename the license and remove any 328 | references to the name of the license steward (except to note that such 329 | modified license differs from this License). 330 | 331 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 332 | If You choose to distribute Source Code Form that is Incompatible With 333 | Secondary Licenses under the terms of this version of the License, the 334 | notice described in Exhibit B of this License must be attached. 335 | 336 | Exhibit A - Source Code Form License Notice 337 | 338 | This Source Code Form is subject to the 339 | terms of the Mozilla Public License, v. 340 | 2.0. If a copy of the MPL was not 341 | distributed with this file, You can 342 | obtain one at 343 | http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular file, then 346 | You may include the notice in a location (such as a LICENSE file in a relevant 347 | directory) where a recipient would be likely to look for such a notice. 348 | 349 | You may add additional accurate notices of copyright ownership. 350 | 351 | Exhibit B - “Incompatible With Secondary Licenses” Notice 352 | 353 | This Source Code Form is “Incompatible 354 | With Secondary Licenses”, as defined by 355 | the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /META.d/_summary.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | schema: 1.1 4 | 5 | partition: tf-ecosystem 6 | 7 | summary: 8 | owner: team-tf-hybrid-cloud 9 | description: | 10 | This repo houses the Helm chart to install Terraform Cloud Operator and other associated components. 11 | 12 | visibility: external -------------------------------------------------------------------------------- /META.d/links.yaml: -------------------------------------------------------------------------------- 1 | runbooks: [] 2 | #- name: 3 | # link: 4 | 5 | other_links: [] 6 | #- name: 7 | # link: -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST_IMAGE?=terraform-helm-test 2 | 3 | test-docker: 4 | @docker build --rm -t '$(TEST_IMAGE)' -f $(CURDIR)/test/docker/Test.dockerfile $(CURDIR) 5 | 6 | .PHONY: test-docker 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Cloud Operator Helm Chart 2 | 3 | This repository contains the official HashiCorp Helm chart for installing 4 | and configuring the Terraform Cloud Operator on Kubernetes. This chart supports multiple use 5 | cases of Terraform on Kubernetes depending on the values provided. 6 | 7 | This chart is hosted on the official [HashiCorp helm chart repository](https://helm.releases.hashicorp.com). 8 | 9 | ## Prerequisites 10 | 11 | To use the charts here, [Helm](https://helm.sh/) must be installed in your 12 | Kubernetes cluster. Setting up Kubernetes and Helm and is outside the scope 13 | of this README. Please refer to the Kubernetes and Helm documentation. 14 | 15 | The versions required are: 16 | 17 | * **Helm +3.0.1** - This is the earliest version of Helm tested. It is possible 18 | it works with earlier versions but this chart is untested for those versions. 19 | * **Kubernetes 1.15+** - This is the earliest version of Kubernetes tested. 20 | It is possible that this chart works with earlier versions but it is 21 | untested. 22 | 23 | In addition to Helm, you must also have a: 24 | 25 | * **Terraform Cloud organization** - Create an organization on Terraform 26 | Cloud/Enterprise. 27 | * **Terraform Cloud Team API Token** - Generate a 28 | [team API token](https://www.terraform.io/docs/cloud/users-teams-organizations/api-tokens.html) for the 29 | Terraform Cloud organization you want to use. Make sure the team at least 30 | has privileges to manage workspaces. 31 | 32 | ## Usage 33 | 34 | Before installing the chart, you must create two Kubernetes secrets: 35 | 36 | 1. `credentials` file contents with Terraform Cloud Team API token. See 37 | [Terraform Cloud Configuration File Syntax](https://www.terraform.io/docs/commands/cli-config.html) 38 | for proper format. 39 | ```shell 40 | $ kubectl -n $NAMESPACE create secret generic terraformrc --from-file=credentials 41 | ``` 42 | 43 | 1. Sensitive variables for a workspace. 44 | ```shell 45 | $ kubectl -n $NAMESPACE create secret generic workspacesecrets --from-literal=secret_key=abc123 46 | ``` 47 | 48 | To use the charts, you must add the HashiCorp Helm Chart repository. 49 | 50 | ```shell 51 | $ helm repo add hashicorp https://helm.releases.hashicorp.com 52 | $ helm search repo hashicorp/terraform 53 | $ helm install --namespace ${RELEASE_NAMESPACE} hashicorp/terraform --generate-name 54 | ``` 55 | ``` 56 | NAME CHART VERSION APP VERSION DESCRIPTION 57 | hashicorp/terraform 1.0 Install and configure Terraform Cloud Operator ... 58 | ``` 59 | 60 | ``` 61 | NAME: terraform-1589480669 62 | LAST DEPLOYED: Thu May 14 11:24:32 2020 63 | NAMESPACE: operator 64 | STATUS: deployed 65 | REVISION: 1 66 | NOTES: 67 | Thank you for installing HashiCorp Terraform Cloud Operator! 68 | 69 | Now that you have deployed HashiCorp Terraform Cloud Operator, you should look over the docs on using 70 | Terraform with Kubernetes available here: 71 | 72 | https://github.com/hashicorp/terraform-k8s/blob/master/README.md 73 | 74 | 75 | Your release is named terraform-1589480669. To learn more about the release, try: 76 | 77 | $ helm status terraform-1589480669 78 | $ helm get terraform-1589480669 79 | ``` 80 | 81 | 82 | Please see the many options supported in the `values.yaml` 83 | file. 84 | 85 | To create a Terraform workspace, you can create a separate Helm chart to deploy 86 | the custom resource or examine the example under `example/`. Helm does not currently 87 | support a `wait` function before deletion, which will cause custom resources to remain 88 | behind. 89 | 90 | Note that the Helm chart automatically installs all Custom Resource Definitions under 91 | the `crds/` directory. As a result, any updates to the schema must be manually copied into 92 | the directory and removed from the Kubernetes cluster: 93 | 94 | ```shell 95 | $ kubectl delete crd workspaces.app.terraform.io 96 | ``` 97 | 98 | If the CRD is not updated correctly, you will not be able to create a Workspace Custom Resource. 99 | -------------------------------------------------------------------------------- /crds/app.terraform.io_workspaces_crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: workspaces.app.terraform.io 9 | spec: 10 | group: app.terraform.io 11 | names: 12 | kind: Workspace 13 | listKind: WorkspaceList 14 | plural: workspaces 15 | singular: workspace 16 | scope: Namespaced 17 | versions: 18 | - additionalPrinterColumns: 19 | - jsonPath: .metadata.creationTimestamp 20 | name: Age 21 | type: date 22 | - jsonPath: .status.runStatus 23 | name: Status 24 | type: string 25 | name: v1alpha1 26 | schema: 27 | openAPIV3Schema: 28 | description: Workspace is the Schema for the workspaces API 29 | properties: 30 | apiVersion: 31 | description: 'APIVersion defines the versioned schema of this representation 32 | of an object. Servers should convert recognized schemas to the latest 33 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 34 | type: string 35 | kind: 36 | description: 'Kind is a string value representing the REST resource this 37 | object represents. Servers may infer this from the endpoint the client 38 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 39 | type: string 40 | metadata: 41 | type: object 42 | spec: 43 | description: WorkspaceSpec defines the desired state of Workspace 44 | properties: 45 | agentPoolID: 46 | description: Specifies the agent pool ID we wish to use. 47 | type: string 48 | agentPoolName: 49 | description: Specifies the agent pool name we wish to use. 50 | type: string 51 | module: 52 | description: Module source and version to use 53 | nullable: true 54 | properties: 55 | source: 56 | description: Any remote module source (version control, registry) 57 | type: string 58 | version: 59 | description: Module version for registry modules 60 | type: string 61 | required: 62 | - source 63 | type: object 64 | notifications: 65 | description: Notification configuration 66 | items: 67 | description: Notification notification holds all the necessary information 68 | required to configure any workspace notification 69 | properties: 70 | enabled: 71 | description: Control if the notification is enabled or not 72 | type: boolean 73 | name: 74 | description: Name of the hook 75 | type: string 76 | recipients: 77 | description: List of recipients' email addresses. Only applicable 78 | for TFE endpoints. 79 | items: 80 | type: string 81 | type: array 82 | token: 83 | description: Token used to generate an HMAC on the verificatio0n 84 | request 85 | type: string 86 | triggers: 87 | description: When the web hook gets triggered. Acceptable values 88 | are run:created, run:planning, run:needs_attention, run:applying, 89 | run:completed, run:errored. 90 | items: 91 | type: string 92 | type: array 93 | type: 94 | description: Notification type. Can be one of email, generic, 95 | or slack 96 | type: string 97 | url: 98 | description: URL of the hook 99 | type: string 100 | users: 101 | description: List of users to receive the notificaiton email. 102 | items: 103 | type: string 104 | type: array 105 | required: 106 | - enabled 107 | - name 108 | - type 109 | type: object 110 | type: array 111 | omitNamespacePrefix: 112 | description: Omit namespace prefix in workspace name 113 | type: boolean 114 | organization: 115 | description: Terraform Cloud organization 116 | type: string 117 | outputs: 118 | description: Outputs denote outputs wanted 119 | items: 120 | description: OutputSpec specifies which values need to be output 121 | properties: 122 | key: 123 | description: Output name 124 | type: string 125 | moduleOutputName: 126 | description: Attribute name in module 127 | type: string 128 | type: object 129 | type: array 130 | runTriggers: 131 | description: Run Triggers from source workspaces to trigger this workspace 132 | items: 133 | description: Run Trigger from a source workspace 134 | properties: 135 | sourceableName: 136 | description: Name of source workspace that triggers the current 137 | workspace 138 | type: string 139 | required: 140 | - sourceableName 141 | type: object 142 | type: array 143 | secretsMountPath: 144 | description: File path within operator pod to load workspace secrets 145 | type: string 146 | sshKeyID: 147 | description: SSH Key ID. This key must already exist in the TF Cloud 148 | organization. This can either be the user assigned name of the 149 | SSH Key, or the system assigned ID. 150 | type: string 151 | terraformVersion: 152 | description: Terraform version used for this workspace. The default 153 | is `latest`. 154 | type: string 155 | variables: 156 | description: Variables as inputs to module 157 | items: 158 | description: Variable denotes an input to the module 159 | properties: 160 | environmentVariable: 161 | description: EnvironmentVariable denotes if this variable should 162 | be created as environment variable 163 | type: boolean 164 | hcl: 165 | description: String input should be an HCL-formatted variable 166 | type: boolean 167 | key: 168 | description: Variable name 169 | type: string 170 | sensitive: 171 | description: Variable is a secret and should be retrieved from 172 | file 173 | type: boolean 174 | value: 175 | description: Variable value 176 | type: string 177 | valueFrom: 178 | description: Source for the variable's value. Cannot be used 179 | if value is not empty. 180 | properties: 181 | configMapKeyRef: 182 | description: Selects a key of a ConfigMap. 183 | properties: 184 | key: 185 | description: The key to select. 186 | type: string 187 | name: 188 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 189 | TODO: Add other useful fields. apiVersion, kind, uid?' 190 | type: string 191 | optional: 192 | description: Specify whether the ConfigMap or its key 193 | must be defined 194 | type: boolean 195 | required: 196 | - key 197 | type: object 198 | fieldRef: 199 | description: 'Selects a field of the pod: supports metadata.name, 200 | metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, 201 | spec.nodeName, spec.serviceAccountName, status.hostIP, 202 | status.podIP, status.podIPs.' 203 | properties: 204 | apiVersion: 205 | description: Version of the schema the FieldPath is 206 | written in terms of, defaults to "v1". 207 | type: string 208 | fieldPath: 209 | description: Path of the field to select in the specified 210 | API version. 211 | type: string 212 | required: 213 | - fieldPath 214 | type: object 215 | resourceFieldRef: 216 | description: 'Selects a resource of the container: only 217 | resources limits and requests (limits.cpu, limits.memory, 218 | limits.ephemeral-storage, requests.cpu, requests.memory 219 | and requests.ephemeral-storage) are currently supported.' 220 | properties: 221 | containerName: 222 | description: 'Container name: required for volumes, 223 | optional for env vars' 224 | type: string 225 | divisor: 226 | anyOf: 227 | - type: integer 228 | - type: string 229 | description: Specifies the output format of the exposed 230 | resources, defaults to "1" 231 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 232 | x-kubernetes-int-or-string: true 233 | resource: 234 | description: 'Required: resource to select' 235 | type: string 236 | required: 237 | - resource 238 | type: object 239 | secretKeyRef: 240 | description: Selects a key of a secret in the pod's namespace 241 | properties: 242 | key: 243 | description: The key of the secret to select from. Must 244 | be a valid secret key. 245 | type: string 246 | name: 247 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 248 | TODO: Add other useful fields. apiVersion, kind, uid?' 249 | type: string 250 | optional: 251 | description: Specify whether the Secret or its key must 252 | be defined 253 | type: boolean 254 | required: 255 | - key 256 | type: object 257 | type: object 258 | required: 259 | - environmentVariable 260 | - key 261 | - sensitive 262 | type: object 263 | type: array 264 | vcs: 265 | description: Details of the VCS repository we want to connect to the 266 | workspace 267 | nullable: true 268 | properties: 269 | branch: 270 | description: The repository branch to use 271 | type: string 272 | ingress_submodules: 273 | description: Whether submodules should be fetched when cloning 274 | the VCS repository (Defaults to false) 275 | type: boolean 276 | repo_identifier: 277 | description: A reference to your VCS repository in the format 278 | org/repo 279 | type: string 280 | token_id: 281 | description: Token ID of the VCS Connection (OAuth Connection 282 | Token) to use https://www.terraform.io/docs/cloud/vcs 283 | type: string 284 | required: 285 | - repo_identifier 286 | - token_id 287 | type: object 288 | required: 289 | - organization 290 | - secretsMountPath 291 | type: object 292 | status: 293 | description: WorkspaceStatus defines the observed state of Workspace 294 | properties: 295 | configVersionID: 296 | description: Configuration Version ID 297 | type: string 298 | outputs: 299 | description: Outputs from state file 300 | items: 301 | description: OutputStatus outputs the values of Terraform output 302 | properties: 303 | key: 304 | description: Attribute name in module 305 | type: string 306 | value: 307 | description: Value 308 | type: string 309 | type: object 310 | type: array 311 | runID: 312 | description: Run ID 313 | type: string 314 | runStatus: 315 | description: Run Status gets the run status 316 | type: string 317 | workspaceID: 318 | description: Workspace ID 319 | type: string 320 | required: 321 | - configVersionID 322 | - runID 323 | - runStatus 324 | - workspaceID 325 | type: object 326 | type: object 327 | served: true 328 | storage: true 329 | subresources: 330 | status: {} 331 | status: 332 | acceptedNames: 333 | kind: "" 334 | plural: "" 335 | conditions: [] 336 | storedVersions: [] 337 | -------------------------------------------------------------------------------- /docs/workspace-sync.html.md: -------------------------------------------------------------------------------- 1 | # Syncing Kubernetes and Terraform Cloud Workspaces 2 | 3 | The [Terraform Operator for 4 | Kubernetes](https://github.com/hashicorp/terraform-k8s) allows you to define a 5 | `Workspace` Custom Resource in Kubernetes that automatically syncs workspace 6 | definitions in Kubernetes to workspaces in Terraform Cloud, using the Kubernetes 7 | [Operator 8 | pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). You 9 | can automatically install and configure the terraform-k8s project using the 10 | [Terraform Helm chart](https://github.com/hashicorp/terraform-helm). 11 | 12 | **Why create a Terraform Cloud Workspace Custom Resource Definition (CRD) for 13 | Kubernetes?** The Terraform Cloud Workspace CRD allows applications deployed in 14 | Kubernetes to define their own infrastructure configuration using Kubernetes 15 | resources. The functionality depends on Terraform Cloud to ensure consistent 16 | approaches to state locking, state storage, and execution. 17 | 18 | **Why sync Kubernetes workspaces to Terraform Cloud?** Syncing Kubernetes 19 | workspaces to Terraform Cloud provides a first-class Kubernetes interface for 20 | updating infrastructure managed by Terraform Cloud by re-executing updates to 21 | infrastructure configuration and Terraform Cloud non-sensitive variables. 22 | 23 | **How does it work?** The workspace sync uses a Terraform Operator in the 24 | [terraform-k8s project](https://github.com/hashicorp/terraform-k8s). The 25 | Operator must run within a Kubernetes cluster and be scoped to a namespace. A 26 | [Helm chart](https://github.com/hashicorp/terraform-helm) automatically deploys 27 | the operator and resource definition. 28 | 29 | **How does the operator handle sensitive variables for Terraform Cloud?** There 30 | are two categories of sensitive variables related to Terraform Cloud: 31 | 1. Terraform Cloud API Token: used to log in and execute runs for Terraform 32 | Cloud 33 | 2. Workspace Sensitive Variables: secrets that execution requires to log into 34 | providers (e.g., credentials). 35 | 36 | See the [Authentication](#authentication) and [Workspace Sensitive 37 | Variables](#workspace-sensitive-variables) sections for how these are handled. 38 | 39 | ## Installation and Configuration 40 | 41 | ### Namespace 42 | 43 | Create the namespace where you will deploy the Operator, Secrets, and Workspace 44 | resources. 45 | 46 | ```shell 47 | $ kubectl create ns $NAMESPACE 48 | ``` 49 | 50 | ### Authentication 51 | 52 | The operator must authenticate to Terraform Cloud. Note that `terraform-k8s` 53 | must run within the cluster, which means that it already handles Kubernetes 54 | authentication. 55 | 56 | 1. Generate a Terraform Cloud Team API token at 57 | `https://app.terraform.io/app/$ORGANIZATION/settings/teams`, where 58 | `$ORGANIZATION` is your organization name. 59 | 60 | 1. Create a file for storing the API token and open it in a text editor. 61 | 62 | 1. Insert the generated token (`$TERRAFORM_CLOUD_API_TOKEN`) into the 63 | text file formatted for Terraform credentials. 64 | ```hcl 65 | credentials app.terraform.io { 66 | token = "$TERRAFORM_CLOUD_API_TOKEN" 67 | } 68 | ``` 69 | 70 | 1. Create a Kubernetes secret named `terraformrc` in the namespace. 71 | Reference the credentials file (`$FILENAME`) created in the previous step. 72 | ```shell 73 | $ kubectl create -n $NAMESPACE secret generic terraformrc --from-file=credentials=$FILENAME 74 | ``` 75 | Ensure `terraformrc` is the name of the secret, as it is the default secret 76 | name defined under the Helm value `syncWorkspace.terraformRC.secretName` in 77 | the `values.yaml` file. 78 | 79 | If you have the free tier of Terraform Cloud, you will only be able to generate 80 | a token for the one team associated with your account. If you have a paid tier 81 | of Terraform Cloud, create a separate team for the `operator` with "Manage 82 | Workspaces" access. 83 | 84 | Note that a Terraform Cloud Team API token is a broad-spectrum token. It allows 85 | the token holder to create workspaces and execute Terraform runs. You cannot 86 | limit the access it provides to a single workspace or role within a team. In 87 | order to support a first-class Kubernetes experience, security and access 88 | control to this token must be enforced by [Kubernetes Role-Based Access Control 89 | (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) policies. 90 | 91 | ### Workspace Sensitive Variables 92 | 93 | Sensitive variables in Terraform Cloud workspaces often take the form of 94 | credentials for cloud providers or API endpoints. They enable Terraform Cloud to 95 | authenticate against a provider and apply changes to infrastructure. 96 | 97 | Create the secret for the namespace that contains all of the sensitive variables 98 | required for the workspace. 99 | 100 | ```shell 101 | $ kubectl create -n $NAMESPACE secret generic workspacesecrets --from-literal=SECRET_KEY=$SECRET_KEY --from-literal=SECRET_KEY_2=$SECRET_KEY_2 ... 102 | ``` 103 | Ensure `workspacesecrets` is the name of the secret, as it is the default secret 104 | name defined under the Helm value `syncWorkspace.sensitiveVariables.secretName` in 105 | the `values.yaml` file. 106 | 107 | In order to support a first-class Kubernetes experience, security and access 108 | control to these secrets must be enforced by [Kubernetes Role-Based Access 109 | Control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) 110 | policies. 111 | 112 | ### Terraform Version 113 | 114 | By default, the Operator will create a Terraform Cloud/Enterprise workspace with 115 | [a pinned version](https://github.com/hashicorp/terraform-k8s/blob/master/operator/version/version.go) 116 | of Terraform. 117 | 118 | Override the Terraform version that will be set for the workspace by changing the 119 | Helm value `syncWorkspace.terraformVersion` to the Terraform version of choice. 120 | 121 | ## Deploy the Operator 122 | 123 | Use the [Helm chart](https://github.com/hashicorp/terraform-helm) repository to 124 | deploy the Terraform Operator to the namespace you previously created. 125 | 126 | ```shell 127 | $ helm install -n $NAMESPACE operator ./terraform-helm 128 | ``` 129 | 130 | ## Create a Workspace 131 | 132 | The Workspace CustomResource defines a Terraform Cloud workspace, including 133 | variables, Terraform module, and outputs. 134 | 135 | For examples of Workspace CustomResource, see 136 | `example/`. 137 | 138 | The Workspace Spec includes the following parameters: 139 | 140 | 1. `organization`: The Terraform Cloud organization you would like to use. 141 | 142 | 1. `secretsMountPath`: The file path defined on the operator deployment that 143 | contains the workspace's secrets. 144 | 145 | Additional parameters are outlined below. 146 | 147 | ### Modules 148 | 149 | > The Workspace will only execute Terraform configuration in a module. It will 150 | > not execute `*.tf` files. 151 | 152 | Information passed to the Workspace CustomResource will be rendered to a 153 | template Terraform configuration that uses the `module` block. Specify a module 154 | with remote `source`. Publicly available VCS repositories, the Terraform 155 | Registry, and private module registry are supported. In addition to `source`, 156 | specify a module `version`. 157 | 158 | ```yaml 159 | module: 160 | source: "hashicorp/hello/random" 161 | version: "3.1.0" 162 | ``` 163 | 164 | The above Kubernetes definition renders to the following Terraform 165 | configuration. 166 | 167 | ```hcl 168 | module "operator" { 169 | source = "hashicorp/hello/random" 170 | version = "3.1.0" 171 | } 172 | ``` 173 | 174 | ### Variables 175 | 176 | Variables for the workspace must equal the module's input variables. 177 | You can define Terraform variables in two ways: 178 | 179 | 1. Inline 180 | ```yaml 181 | variables: 182 | - key: hello 183 | value: world 184 | sensitive: false 185 | environmentVariable: false 186 | ``` 187 | 188 | 2. With a Kubernetes ConfigMap reference 189 | ```yaml 190 | variables: 191 | - key: second_hello 192 | valueFrom: 193 | configMapKeyRef: 194 | name: say-hello 195 | key: to 196 | sensitive: false 197 | environmentVariable: false 198 | ``` 199 | 200 | The above Kubernetes definition renders to the following Terraform 201 | configuration. 202 | 203 | ```hcl 204 | variable "hello" {} 205 | 206 | variable "second_hello" {} 207 | 208 | module "operator" { 209 | source = "hashicorp/hello/random" 210 | version = "3.1.0" 211 | hello = var.hello 212 | second_hello = var.second_hello 213 | } 214 | ``` 215 | 216 | The operator pushes the values of the variables to the Terraform Cloud 217 | workspace. For secrets, set `sensitive` to be `true`. The workspace sets them as 218 | write-only. Denote workspace environment variables by setting 219 | `environmentVariable` as `true`. 220 | 221 | Sensitive variables should already be 222 | initialized as per 223 | [Workspace Sensitive Variables](#workspace-sensitive-variables). You can define 224 | them by setting `sensitive: true`. Do not define the value or use a ConfigMap 225 | reference, as the read from file will override the value you set. 226 | 227 | ```yaml 228 | variables: 229 | - key: AWS_SECRET_ACCESS_KEY 230 | sensitive: true 231 | environmentVariable: true 232 | ``` 233 | 234 | ### Apply an SSH key to the Workspace (optional) 235 | 236 | SSH keys can be used to [clone private modules](https://www.terraform.io/docs/cloud/workspaces/ssh-keys.html). To apply an SSH key to the workspace, specify `sshKeyID` in the Workspace Custom Resource. The SSH key ID can be found in the [Terraform Cloud API](https://www.terraform.io/docs/cloud/api/ssh-keys.html#list-ssh-keys). 237 | 238 | ``` 239 | apiVersion: app.terraform.io/v1alpha1 240 | kind: Workspace 241 | metadata: 242 | name: $WORKSPACE 243 | spec: 244 | sshKeyID: $SSHKEYID 245 | ``` 246 | 247 | ### Outputs 248 | 249 | In order to retrieve Terraform outputs, specify the `outputs` 250 | section of the Workspace CustomResource. The `key` represents the output key you 251 | expect from `terraform output` and `moduleOutputName` denotes the module's 252 | output key name. 253 | 254 | ```yaml 255 | outputs: 256 | - key: my_pet 257 | moduleOutputName: pet 258 | ``` 259 | 260 | The above Kubernetes definition renders to the following Terraform 261 | configuration. 262 | 263 | ```hcl 264 | output "my_pet" { 265 | value = module.operator.pet 266 | } 267 | ``` 268 | 269 | The values of the outputs can be consumed from two places: 270 | 271 | 1. Kubernetes status of the workspace. 272 | ```shell 273 | $ kubectl describe -n $NAMESPACE workspace $WORKSPACE_NAME 274 | ``` 275 | 1. ConfigMap labeled `$WORKSPACE_NAME-outputs`. Kubernetes deployments can consume these 276 | output values. 277 | ```shell 278 | $ kubectl describe -n $NAMESPACE configmap $WORKSPACE_NAME-outputs 279 | ``` 280 | 281 | ### Deploy 282 | 283 | Deploy the workspace after configuring its module, variables, and outputs. 284 | 285 | ```shell 286 | $ kubectl apply -n $NAMESPACE -f workspace.yml 287 | ``` 288 | 289 | ### Update a Workspace 290 | 291 | The following changes updates and executes new runs for the Terraform Cloud workspace: 292 | 293 | 1. `organization` 294 | 1. `module` source or version 295 | 1. `outputs` 296 | 1. Non-sensitive or ConfigMap reference `variables`. 297 | 298 | Updates to sensitive variables *will not* trigger a 299 | new execution because sensitive variables are write-only for security 300 | purposes. The operator is unable to reconcile the upstream value of the 301 | secret with the value stored locally. Similarly, ConfigMap references do not 302 | trigger updates as the operator does not read the value for comparison. 303 | 304 | After updating the configuration, re-deploy the workspace. 305 | 306 | ```shell 307 | $ kubectl apply -n $NAMESPACE -f workspace.yml 308 | ``` 309 | 310 | ### Delete a Workspace 311 | 312 | In order for workspace destruction to work automatically, you must set the 313 | `CONFIRM_DESTROY` environment variable in the Terraform Cloud workspace. When 314 | you delete the Workspace CustomResource, the operator will attempt to destroy 315 | the workspace. As a secondary check, you must deploy the operator with this 316 | environment variable defined in the `variables` section if you would like to 317 | destroy the workspace in Terraform Cloud. 318 | 319 | ```yaml 320 | variables: 321 | - key: CONFIRM_DESTROY 322 | value: "1" 323 | sensitive: false 324 | environmentVariable: true 325 | ``` 326 | 327 | When deleting the Workspace CustomResource, the command line will wait for a few 328 | moments. 329 | 330 | ```shell 331 | $ kubectl delete -n $NAMESPACE workspace.app.terraform.io/$WORKSPACE_NAME 332 | ``` 333 | 334 | This is because the operator is running a 335 | [finalizer](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers). 336 | The finalizer will execute before the workspace officially deletes in order to: 337 | 338 | 1. Stop all runs in the workspace, including pending ones 339 | 1. `terraform destroy -auto-approve` on resources in the workspace 340 | 1. Delete the workspace. 341 | 342 | Once the finalizer completes, Kubernetes deletes the Workspace CustomResource. 343 | 344 | ## Debugging 345 | 346 | Check the status and outputs of the workspace by examining its Kubernetes 347 | status. This provides the run ID and workspace ID to debug in the Terraform 348 | Cloud UI. 349 | 350 | ```shell 351 | $ kubectl describe -n $NAMESPACE workspace $WORKSPACE_NAME 352 | ``` 353 | 354 | When workspace creation, update, or deletion fails, check errors by 355 | examining the logs of the operator. 356 | 357 | ```shell 358 | $ kubectl logs -n $NAMESPACE $(kubectl get pods -n $NAMESPACE --selector "component=sync-workspace" -o jsonpath="{.items[0].metadata.name}") 359 | ``` 360 | 361 | If Terraform Cloud returns an error that the Terraform configuration is 362 | incorrect, examine the Terraform configuration at its ConfigMap. 363 | 364 | ```shell 365 | $ kubectl describe -n $NAMESPACE configmap $WORKSPACE_NAME 366 | ``` 367 | 368 | ## Internals 369 | 370 | ### Why create a namespace and secrets? 371 | 372 | The Helm chart does not include secrets management or injection. Instead, it 373 | expects to find secrets mounted as volumes to the operator's deployment. This 374 | supports secrets management approaches in Kubernetes that use a volume mount for 375 | secrets. 376 | 377 | In order to support a first-class Kubernetes experience, security and access 378 | control to these secrets must be enforced by [Kubernetes Role-Based Access 379 | Control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) 380 | policies. 381 | 382 | For the Terraform Cloud Team API token, the entire credentials file with the Terraform Cloud API Token is mounted to the 383 | filepath specified by `TF_CLI_CONFIG_FILE`. In an equivalent Kubernetes 384 | configuration, the following example creates a Kubernetes secret and mount it to 385 | the operator at the filepath specified by `TF_CLI_CONFIG_FILE`. 386 | 387 | ```yaml 388 | --- 389 | # not secure secrets management 390 | apiVersion: apps/v1 391 | kind: Secret 392 | metadata: 393 | name: terraformrc 394 | type: Opaque 395 | data: 396 | credentials: |- 397 | credentials app.terraform.io { 398 | token = "$TERRAFORM_CLOUD_API_TOKEN" 399 | } 400 | --- 401 | apiVersion: apps/v1 402 | kind: Deployment 403 | metadata: 404 | name: terraform-k8s 405 | spec: 406 | # some sections omitted for clarity 407 | template: 408 | metadata: 409 | labels: 410 | name: terraform-k8s 411 | spec: 412 | serviceAccountName: terraform-k8s 413 | containers: 414 | - name: terraform-k8s 415 | env: 416 | - name: TF_CLI_CONFIG_FILE 417 | value: "/etc/terraform/.terraformrc" 418 | volumeMounts: 419 | - name: terraformrc 420 | mountPath: "/etc/terraform" 421 | readOnly: true 422 | volumes: 423 | - name: terraformrc 424 | secret: 425 | secretName: terraformrc 426 | items: 427 | - key: credentials 428 | path: ".terraformrc" 429 | ``` 430 | 431 | Similar to the Terraform Cloud API Token, the Helm chart mounts 432 | them to the operator's deployment for use. It __does not__ mount workspace 433 | sensitive variables to the Workspace Custom Resource. This ensures that only the operator 434 | has access to read and create sensitive variables as part of the Terraform Cloud 435 | workspace. 436 | 437 | Examine the deployment in `templates/sync-workspace-deployment.yaml`. The 438 | deployment mounts a volume containing the sensitive variables. The file 439 | name is the secret's key and file contents is the secret's value. This 440 | supports secrets management approaches in Kubernetes that use a volume mount for 441 | secrets. 442 | 443 | ```yaml 444 | --- 445 | # not secure secrets management 446 | apiVersion: apps/v1 447 | kind: Secret 448 | metadata: 449 | name: workspacesecrets 450 | type: Opaque 451 | data: 452 | AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} 453 | GOOGLE_APPLICATION_CREDENTIALS: ${GOOGLE_APPLICATION_CREDENTIALS} 454 | --- 455 | apiVersion: apps/v1 456 | kind: Deployment 457 | metadata: 458 | name: terraform-k8s 459 | spec: 460 | # some sections omitted for clarity 461 | template: 462 | metadata: 463 | labels: 464 | name: terraform-k8s 465 | spec: 466 | serviceAccountName: terraform-k8s 467 | containers: 468 | - name: terraform-k8s 469 | volumeMounts: 470 | - name: workspacesecrets 471 | mountPath: "/tmp/secrets" 472 | readOnly: true 473 | volumes: 474 | - name: workspacesecrets 475 | secret: 476 | secretName: workspacesecrets 477 | ``` 478 | 479 | ### Helm Chart 480 | 481 | The Helm chart consists of several components. The Kubernetes configurations associated with the 482 | Helm chart are located under `crds/` and `templates/`. 483 | 484 | #### Custom Resource Definition 485 | 486 | Helm starts by deploying the Custom Resource Definition for the Workspace. 487 | Custom Resource Definitions extend the Kubernetes API. It looks for definitions 488 | in the `crds/` of the chart. 489 | 490 | The Custom Resource Definition under `crds/app.terraform.io_workspaces_crd.yaml` 491 | defines that the Workspace Custom Resource schema. 492 | 493 | #### Role-Based Access Control 494 | 495 | In order to scope the operator to a namespace, Helm assigns a role and service 496 | account to the namespace. The role has access to Pods, Secrets, Services, and 497 | ConfigMaps. This configuration is located in `templates/`. 498 | 499 | #### Namespace Scope 500 | 501 | To ensure the operator does not have access to secrets or resource beyond the 502 | namespace, the Helm chart scopes the operator's deployment to a namespace. 503 | 504 | ```yaml 505 | apiVersion: apps/v1 506 | kind: Deployment 507 | metadata: 508 | name: terraform-k8s 509 | spec: 510 | # some sections omitted for clarity 511 | template: 512 | metadata: 513 | labels: 514 | name: terraform-k8s 515 | spec: 516 | serviceAccountName: terraform-k8s 517 | containers: 518 | - name: terraform-k8s 519 | command: 520 | - terraform-k8s 521 | - sync-workspace 522 | - "--k8s-watch-namespace=$(POD_NAMESPACE)" 523 | env: 524 | - name: POD_NAMESPACE 525 | valueFrom: 526 | fieldRef: 527 | fieldPath: metadata.namespace 528 | ``` 529 | 530 | When deploying, ensure that the namespace is passed into the 531 | `--k8s-watch-namespace` option. Otherwise, the operator will attempt to access 532 | across all namespaces (cluster scope). 533 | -------------------------------------------------------------------------------- /example/configmap.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: aws-configuration 6 | data: 7 | region: us-east-1 -------------------------------------------------------------------------------- /example/greetings/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mesosphere/aws-cli 2 | 3 | COPY message.sh /project/message.sh -------------------------------------------------------------------------------- /example/greetings/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: greetings 5 | labels: 6 | app: greetings 7 | spec: 8 | template: 9 | metadata: 10 | labels: 11 | app: greetings 12 | spec: 13 | restartPolicy: Never 14 | containers: 15 | - name: greetings 16 | image: joatmon08/aws-sqs-test 17 | command: ["./message.sh"] 18 | env: 19 | - name: QUEUE_URL 20 | valueFrom: 21 | configMapKeyRef: 22 | name: greetings-outputs 23 | key: url 24 | - name: AWS_DEFAULT_REGION 25 | valueFrom: 26 | configMapKeyRef: 27 | name: aws-configuration 28 | key: region 29 | volumeMounts: 30 | - name: sensitivevars 31 | mountPath: "/tmp/secrets" 32 | readOnly: true 33 | volumes: 34 | - name: sensitivevars 35 | secret: 36 | secretName: workspacesecrets 37 | -------------------------------------------------------------------------------- /example/greetings/message.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | export AWS_ACCESS_KEY_ID=$(cat /tmp/secrets/AWS_ACCESS_KEY_ID) 5 | export AWS_SECRET_ACCESS_KEY=$(cat /tmp/secrets/AWS_SECRET_ACCESS_KEY) 6 | 7 | echo "sending a message to queue $QUEUE_URL" 8 | aws sqs send-message --queue-url $QUEUE_URL --message-body "hello world!" --message-group-id "greetings" --message-deduplication-id "world" 9 | 10 | echo "reading a message from queue $QUEUE_URL" 11 | aws sqs receive-message --queue-url $QUEUE_URL -------------------------------------------------------------------------------- /example/workspace_git.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: app.terraform.io/v1alpha1 3 | kind: Workspace 4 | metadata: 5 | name: salutations 6 | spec: 7 | # Optional field for SSH Key ID. To get the id you need to list the keys using the API: https://www.terraform.io/docs/cloud/api/ssh-keys.html#list-ssh-keys. 8 | # sshKeyID: sshkey-D4fmNfDCWAoBJopq 9 | organization: hashicorp-team-demo 10 | secretsMountPath: "/tmp/secrets" 11 | module: 12 | source: "git::https://github.com/joatmon08/queues.git" 13 | outputs: 14 | - key: queue_urls 15 | moduleOutputName: queue_urls 16 | - key: json_string 17 | moduleOutputName: json_string 18 | variables: 19 | - key: application 20 | value: goodbye 21 | sensitive: false 22 | environmentVariable: false 23 | - key: environment 24 | value: preprod 25 | sensitive: false 26 | environmentVariable: false 27 | - key: some_list 28 | value: '["hello","queues"]' 29 | hcl: true 30 | sensitive: false 31 | environmentVariable: false 32 | - key: some_object 33 | value: '{hello={name= "test"}}' 34 | hcl: true 35 | sensitive: false 36 | environmentVariable: false 37 | - key: AWS_DEFAULT_REGION 38 | valueFrom: 39 | configMapKeyRef: 40 | name: aws-configuration 41 | key: region 42 | sensitive: false 43 | environmentVariable: true 44 | - key: AWS_ACCESS_KEY_ID 45 | sensitive: true 46 | environmentVariable: true 47 | - key: AWS_SECRET_ACCESS_KEY 48 | sensitive: true 49 | environmentVariable: true 50 | - key: CONFIRM_DESTROY 51 | value: "1" 52 | sensitive: false 53 | environmentVariable: true 54 | -------------------------------------------------------------------------------- /example/workspace_random.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: app.terraform.io/v1alpha1 3 | kind: Workspace 4 | metadata: 5 | name: "random" 6 | spec: 7 | organization: tf-operator 8 | secretsMountPath: "/tmp/secrets" 9 | module: 10 | source: "joatmon08/hello/random" 11 | version: "5.0.0" 12 | outputs: 13 | - key: pet 14 | moduleOutputName: pet 15 | - key: list_of_pets 16 | moduleOutputName: list_of_pets 17 | - key: quoted_some_key 18 | moduleOutputName: quoted_some_key 19 | variables: 20 | - key: hellos 21 | value: | 22 | { 23 | hello="world" 24 | second_hello="universe" 25 | } 26 | hcl: true 27 | sensitive: false 28 | environmentVariable: false 29 | - key: some_key 30 | sensitive: false 31 | value: '"something"' 32 | environmentVariable: false 33 | - key: CONFIRM_DESTROY 34 | value: "1" 35 | sensitive: false 36 | environmentVariable: true -------------------------------------------------------------------------------- /example/workspace_registry.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: app.terraform.io/v1alpha1 3 | kind: Workspace 4 | metadata: 5 | name: greetings 6 | spec: 7 | organization: hashicorp-team-demo 8 | secretsMountPath: "/tmp/secrets" 9 | module: 10 | source: "terraform-aws-modules/sqs/aws" 11 | version: "2.0.0" 12 | outputs: 13 | - key: url 14 | moduleOutputName: this_sqs_queue_id 15 | - key: arn 16 | moduleOutputName: this_sqs_queue_arn 17 | variables: 18 | - key: name 19 | value: greetings.fifo 20 | sensitive: false 21 | environmentVariable: false 22 | - key: fifo_queue 23 | value: "true" 24 | sensitive: false 25 | environmentVariable: false 26 | - key: AWS_DEFAULT_REGION 27 | valueFrom: 28 | configMapKeyRef: 29 | name: aws-configuration 30 | key: region 31 | sensitive: false 32 | environmentVariable: true 33 | - key: AWS_ACCESS_KEY_ID 34 | sensitive: true 35 | environmentVariable: true 36 | - key: AWS_SECRET_ACCESS_KEY 37 | sensitive: true 38 | environmentVariable: true 39 | - key: CONFIRM_DESTROY 40 | value: "1" 41 | sensitive: false 42 | environmentVariable: true -------------------------------------------------------------------------------- /example/workspace_remote_agent.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.terraform.io/v1alpha1 2 | kind: Workspace 3 | metadata: 4 | name: demo 5 | spec: 6 | organization: demo 7 | secretsMountPath: "/tmp/secrets" 8 | module: 9 | source: "github.com/koikonom/terraform-nullresource-example" 10 | # The agent pool ID can be found either in the Agent Pool's config page 11 | # (https://app.terraform.io/app//settings/agents) 12 | # or in the agent's startup messages 13 | agentPoolID: apool- 14 | variables: 15 | - key: CONFIRM_DESTROY 16 | value: "1" 17 | sensitive: false 18 | environmentVariable: true 19 | outputs: 20 | - key: pet 21 | moduleOutputName: rofl 22 | -------------------------------------------------------------------------------- /example/workspace_vcs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.terraform.io/v1alpha1 2 | kind: Workspace 3 | metadata: 4 | name: demo 5 | spec: 6 | organization: demo 7 | secretsMountPath: "/tmp/secrets" 8 | module: 9 | source: "github.com/koikonom/terraform-nullresource-example" 10 | vcs: 11 | # Your OAuth Token ID can be found at https://app.terraform.io/app//settings/version-control 12 | token_id: "ot-" 13 | repo_identifier: "koikonom/terraform-nullresource-example" 14 | ingress_submodules: false 15 | variables: 16 | - key: CONFIRM_DESTROY 17 | value: "1" 18 | sensitive: false 19 | environmentVariable: true 20 | outputs: 21 | - key: pet 22 | moduleOutputName: rofl 23 | -------------------------------------------------------------------------------- /templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 2 | Thank you for installing HashiCorp Terraform Cloud Operator! 3 | 4 | Now that you have deployed HashiCorp Terraform Cloud Operator, you should look over the docs on using 5 | Terraform with Kubernetes available here: 6 | 7 | https://github.com/hashicorp/terraform-k8s/blob/master/README.md 8 | 9 | 10 | Your release is named {{ .Release.Name }}. To learn more about the release, try: 11 | 12 | $ helm status {{ .Release.Name }} 13 | $ helm get {{ .Release.Name }} 14 | -------------------------------------------------------------------------------- /templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Create a default fully qualified app name. 3 | We truncate at 63 chars because some Kubernetes name fields are limited to 4 | this (by the DNS naming spec). If release name contains chart name it will 5 | be used as a full name. 6 | */}} 7 | {{- define "terraform.fullname" -}} 8 | {{- if .Values.fullnameOverride -}} 9 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 10 | {{- else -}} 11 | {{- $name := default .Chart.Name .Values.nameOverride -}} 12 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 13 | {{- end -}} 14 | {{- end -}} 15 | 16 | {{/* 17 | Create chart name and version as used by the chart label. 18 | */}} 19 | {{- define "terraform.chart" -}} 20 | {{- printf "%s-helm" .Chart.Name | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 21 | {{- end -}} 22 | 23 | {{/* 24 | Expand the name of the chart. 25 | */}} 26 | {{- define "terraform.name" -}} 27 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 28 | {{- end -}} 29 | 30 | {{/* 31 | Compute the maximum number of unavailable replicas for the PodDisruptionBudget. 32 | This defaults to (n/2)-1 where n is the number of members of the server cluster. 33 | Special case of replica equaling 3 and allowing a minor disruption of 1 otherwise 34 | use the integer value 35 | Add a special case for replicas=1, where it should default to 0 as well. 36 | */}} 37 | {{- define "terraform.pdb.maxUnavailable" -}} 38 | {{- if eq (int .Values.server.replicas) 1 -}} 39 | {{ 0 }} 40 | {{- else if .Values.server.disruptionBudget.maxUnavailable -}} 41 | {{ .Values.server.disruptionBudget.maxUnavailable -}} 42 | {{- else -}} 43 | {{- if eq (int .Values.server.replicas) 3 -}} 44 | {{- 1 -}} 45 | {{- else -}} 46 | {{- sub (div (int .Values.server.replicas) 2) 1 -}} 47 | {{- end -}} 48 | {{- end -}} 49 | {{- end -}} 50 | 51 | {{/* 52 | Inject extra environment vars in the format key:value, if populated 53 | */}} 54 | {{- define "terraform.extraEnvironmentVars" -}} 55 | {{- if .extraEnvironmentVars -}} 56 | {{- range $key, $value := .extraEnvironmentVars }} 57 | - name: {{ $key }} 58 | value: {{ $value | quote }} 59 | {{- end -}} 60 | {{- end -}} 61 | {{- end -}} 62 | -------------------------------------------------------------------------------- /templates/sync-workspace-deployment.yaml: -------------------------------------------------------------------------------- 1 | # The deployment for running the sync-catalog pod 2 | {{- if (or (and (ne (.Values.syncWorkspace.enabled | toString) "-") .Values.syncWorkspace.enabled) (and (eq (.Values.syncWorkspace.enabled | toString) "-") .Values.global.enabled)) }} 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: {{ template "terraform.fullname" . }}-sync-workspace 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | app: {{ template "terraform.name" . }} 10 | chart: {{ template "terraform.chart" . }} 11 | heritage: {{ .Release.Service }} 12 | release: {{ .Release.Name }} 13 | spec: 14 | replicas: 1 15 | selector: 16 | matchLabels: 17 | app: {{ template "terraform.name" . }} 18 | chart: {{ template "terraform.chart" . }} 19 | release: {{ .Release.Name }} 20 | component: sync-workspace 21 | template: 22 | metadata: 23 | labels: 24 | app: {{ template "terraform.name" . }} 25 | chart: {{ template "terraform.chart" . }} 26 | release: {{ .Release.Name }} 27 | component: sync-workspace 28 | spec: 29 | serviceAccountName: {{ template "terraform.fullname" . }}-sync-workspace 30 | containers: 31 | - name: terraform-sync-workspace 32 | image: "{{ default .Values.global.imageK8S .Values.syncWorkspace.image }}" 33 | imagePullPolicy: "{{ default "IfNotPresent" .Values.syncWorkspace.imagePullPolicy }}" 34 | env: 35 | - name: POD_NAME 36 | valueFrom: 37 | fieldRef: 38 | fieldPath: metadata.name 39 | - name: OPERATOR_NAME 40 | value: "terraform-k8s" 41 | {{- if .Values.syncWorkspace.terraformVersion }} 42 | - name: TF_VERSION 43 | value: "{{ .Values.syncWorkspace.terraformVersion }}" 44 | {{- end }} 45 | {{- if .Values.syncWorkspace.insecure }} 46 | - name: TF_INSECURE 47 | value: "{{ .Values.syncWorkspace.insecure }}" 48 | {{- end}} 49 | - name: TF_CLI_CONFIG_FILE 50 | value: "/etc/terraform/.terraformrc" 51 | - name: TF_URL 52 | value: "{{ default "" .Values.syncWorkspace.tfeAddress }}" 53 | volumeMounts: 54 | {{- if .Values.CACerts}} 55 | - name: cacert 56 | mountPath: "/etc/ssl/certs/" 57 | readOnly: true 58 | {{- end }} 59 | - name: terraformrc 60 | mountPath: "/etc/terraform" 61 | readOnly: true 62 | - name: sensitivevars 63 | mountPath: "/tmp/secrets" 64 | readOnly: true 65 | command: 66 | - /bin/terraform-k8s 67 | args: 68 | - --enable-leader-election 69 | - --k8s-watch-namespace={{ default .Release.Namespace .Values.syncWorkspace.k8WatchNamespace}} 70 | {{- if .Values.syncWorkspace.logLevel }} 71 | - --zap-log-level={{ .Values.syncWorkspace.logLevel }} 72 | {{- end }} 73 | livenessProbe: 74 | httpGet: 75 | path: /metrics 76 | port: 8383 77 | scheme: HTTP 78 | failureThreshold: 3 79 | initialDelaySeconds: 30 80 | periodSeconds: 5 81 | successThreshold: 1 82 | timeoutSeconds: 5 83 | readinessProbe: 84 | httpGet: 85 | path: /metrics 86 | port: 8383 87 | scheme: HTTP 88 | failureThreshold: 5 89 | initialDelaySeconds: 10 90 | periodSeconds: 5 91 | successThreshold: 1 92 | timeoutSeconds: 5 93 | volumes: 94 | {{- if .Values.CACerts}} 95 | - name: cacert 96 | secret: 97 | secretName: cacert 98 | items: 99 | - key: ca_file 100 | path: "ca-certificates.crt" 101 | {{- end }} 102 | - name: terraformrc 103 | secret: 104 | secretName: {{ required "secret name for terraformrc file required" .Values.syncWorkspace.terraformRC.secretName }} 105 | items: 106 | - key: {{ required "secret key for terraformrc file required" .Values.syncWorkspace.terraformRC.secretKey }} 107 | path: ".terraformrc" 108 | - name: sensitivevars 109 | secret: 110 | secretName: {{ required "secret name for sensitive workspace variables required" .Values.syncWorkspace.sensitiveVariables.secretName }} 111 | {{- end }} 112 | -------------------------------------------------------------------------------- /templates/sync-workspace-role.yaml: -------------------------------------------------------------------------------- 1 | {{- $syncEnabled := (or (and (ne (.Values.syncWorkspace.enabled | toString) "-") .Values.syncWorkspace.enabled) (and (eq (.Values.syncWorkspace.enabled | toString) "-") .Values.global.enabled)) }} 2 | {{- if $syncEnabled }} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | {{- $kind := (ternary "Role" "ClusterRole" (or (empty .Values.syncWorkspace.k8WatchNamespace) (eq (.Values.syncWorkspace.k8WatchNamespace | toString) .Release.Namespace))) }} 5 | kind: {{ $kind }} 6 | metadata: 7 | name: {{ template "terraform.fullname" . }}-sync-workspace 8 | labels: 9 | app: {{ template "terraform.name" . }} 10 | chart: {{ template "terraform.chart" . }} 11 | heritage: {{ .Release.Service }} 12 | release: {{ .Release.Name }} 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - pods 18 | - services 19 | - services/finalizers 20 | - endpoints 21 | - persistentvolumeclaims 22 | - events 23 | - configmaps 24 | - secrets 25 | verbs: 26 | - '*' 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - configmaps/status 31 | verbs: 32 | - get 33 | - update 34 | - patch 35 | - apiGroups: 36 | - apps 37 | resources: 38 | - deployments 39 | - daemonsets 40 | - replicasets 41 | - statefulsets 42 | verbs: 43 | - '*' 44 | - apiGroups: 45 | - monitoring.coreos.com 46 | resources: 47 | - servicemonitors 48 | verbs: 49 | - get 50 | - create 51 | - apiGroups: 52 | - apps 53 | resourceNames: 54 | - terraform-k8s 55 | resources: 56 | - deployments/finalizers 57 | verbs: 58 | - update 59 | - apiGroups: 60 | - "" 61 | resources: 62 | - pods 63 | verbs: 64 | - get 65 | - apiGroups: 66 | - apps 67 | resources: 68 | - replicasets 69 | verbs: 70 | - get 71 | - apiGroups: 72 | - app.terraform.io 73 | resources: 74 | - '*' 75 | - workspaces 76 | verbs: 77 | - '*' 78 | - apiGroups: 79 | - coordination.k8s.io 80 | resources: 81 | - leases 82 | verbs: 83 | - create 84 | - get 85 | - update 86 | {{- end }} 87 | -------------------------------------------------------------------------------- /templates/sync-workspace-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- $syncEnabled := (or (and (ne (.Values.syncWorkspace.enabled | toString) "-") .Values.syncWorkspace.enabled) (and (eq (.Values.syncWorkspace.enabled | toString) "-") .Values.global.enabled)) }} 2 | {{- if $syncEnabled }} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | {{- $kind := (ternary "Role" "ClusterRole" (or (empty .Values.syncWorkspace.k8WatchNamespace) (eq (.Values.syncWorkspace.k8WatchNamespace | toString) .Release.Namespace))) }} 5 | kind: {{ ternary "RoleBinding" "ClusterRoleBinding" (eq $kind "Role") }} 6 | metadata: 7 | name: {{ template "terraform.fullname" . }}-sync-workspace 8 | labels: 9 | app: {{ template "terraform.name" . }} 10 | chart: {{ template "terraform.chart" . }} 11 | heritage: {{ .Release.Service }} 12 | release: {{ .Release.Name }} 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ template "terraform.fullname" . }}-sync-workspace 16 | namespace: {{ .Release.Namespace }} 17 | roleRef: 18 | kind: {{ $kind }} 19 | name: {{ template "terraform.fullname" . }}-sync-workspace 20 | apiGroup: rbac.authorization.k8s.io 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /templates/sync-workspace-secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.CACerts }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: cacert 6 | namespace: {{ .Release.Namespace }} 7 | type: Opaque 8 | data: 9 | ca_file: |- 10 | {{ required "CACerts value needs to be set" .Values.CACerts| b64enc | indent 2 }} 11 | {{- end }} 12 | -------------------------------------------------------------------------------- /templates/sync-workspace-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if (or (and (ne (.Values.syncWorkspace.enabled | toString) "-") .Values.syncWorkspace.enabled) (and (eq (.Values.syncWorkspace.enabled | toString) "-") .Values.global.enabled)) }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ template "terraform.fullname" . }}-sync-workspace 6 | labels: 7 | app: {{ template "terraform.name" . }} 8 | chart: {{ template "terraform.chart" . }} 9 | heritage: {{ .Release.Service }} 10 | release: {{ .Release.Name }} 11 | {{- end }} -------------------------------------------------------------------------------- /templates/tests/test-configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tests.enabled }} 2 | --- 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | name: "{{ template "terraform.fullname" . }}-test" 7 | labels: 8 | app: {{ template "terraform.name" . }} 9 | chart: {{ template "terraform.chart" . }} 10 | heritage: {{ .Release.Service }} 11 | release: {{ .Release.Name }} 12 | annotations: 13 | "helm.sh/hook": test 14 | "helm.sh/hook-weight": "0" 15 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 16 | data: 17 | some_key: "for random pets" 18 | backend: | 19 | organization = {{ .Values.tests.organization }} 20 | workspaces { name = "{{ .Release.Namespace }}-{{ template "terraform.fullname" . }}-test" } 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /templates/tests/test-runner.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tests.enabled }} 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: "{{ template "terraform.fullname" . }}-test" 6 | labels: 7 | app: {{ template "terraform.name" . }} 8 | chart: {{ template "terraform.chart" . }} 9 | heritage: {{ .Release.Service }} 10 | release: {{ .Release.Name }} 11 | annotations: 12 | "helm.sh/hook": test 13 | "helm.sh/hook-weight": "2" 14 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 15 | spec: 16 | containers: 17 | - name: terraform-test 18 | image: "{{ .Values.global.imageK8S }}" 19 | imagePullPolicy: "{{ default "IfNotPresent" .Values.syncWorkspace.imagePullPolicy }}" 20 | env: 21 | - name: HOST_IP 22 | valueFrom: 23 | fieldRef: 24 | fieldPath: status.hostIP 25 | volumeMounts: 26 | - name: terraformrc 27 | mountPath: "/etc/terraform" 28 | readOnly: true 29 | - name: backend 30 | mountPath: "/tmp/terraform-test" 31 | readOnly: true 32 | command: 33 | - "/bin/sh" 34 | - "-ec" 35 | - | 36 | sleep 60 37 | terraform init -backend-config=/tmp/terraform-test/backend 38 | terraform output pet 39 | terraform output list_of_pets 40 | restartPolicy: Never 41 | volumes: 42 | - name: terraformrc 43 | secret: 44 | secretName: {{ required "secret name for terraformrc file required" .Values.syncWorkspace.terraformRC.secretName }} 45 | items: 46 | - key: {{ required "secret key for terraformrc file required" .Values.syncWorkspace.terraformRC.secretKey }} 47 | path: ".terraformrc" 48 | - name: backend 49 | configMap: 50 | name: "{{ template "terraform.fullname" . }}-test" 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /templates/tests/test-workspace.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tests.enabled }} 2 | --- 3 | apiVersion: app.terraform.io/v1alpha1 4 | kind: Workspace 5 | metadata: 6 | name: "{{ template "terraform.fullname" . }}-test" 7 | labels: 8 | app: {{ template "terraform.name" . }} 9 | chart: {{ template "terraform.chart" . }} 10 | heritage: {{ .Release.Service }} 11 | release: {{ .Release.Name }} 12 | annotations: 13 | "helm.sh/hook": test 14 | "helm.sh/hook-weight": "1" 15 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 16 | spec: 17 | organization: "{{ .Values.tests.organization }}" 18 | secretsMountPath: "/tmp/secrets" 19 | module: 20 | source: "{{ default "git::https://github.com/hashicorp/terraform-helm.git//test/module" .Values.tests.moduleSource }}" 21 | outputs: 22 | - key: pet 23 | moduleOutputName: pet 24 | - key: list_of_pets 25 | moduleOutputName: list_of_pets 26 | variables: 27 | - key: hellos 28 | value: | 29 | { 30 | hello="world" 31 | second_hello="universe" 32 | } 33 | hcl: true 34 | sensitive: false 35 | environmentVariable: false 36 | - key: some_key 37 | valueFrom: 38 | configMapKeyRef: 39 | name: "{{ template "terraform.fullname" . }}-test" 40 | key: some_key 41 | sensitive: false 42 | environmentVariable: false 43 | - key: CONFIRM_DESTROY 44 | value: "1" 45 | sensitive: false 46 | environmentVariable: true 47 | {{- end }} 48 | -------------------------------------------------------------------------------- /test/acceptance/_helpers.bash: -------------------------------------------------------------------------------- 1 | # name_prefix returns the prefix of the resources within Kubernetes. 2 | name_prefix() { 3 | printf "terraform" 4 | } 5 | 6 | # helm_install installs the Terraform chart. This will source overridable 7 | # values from the "values.yaml" file in this directory. This can be set 8 | # by CI or other environments to do test-specific overrides. Note that its 9 | # easily possible to break tests this way so be careful. 10 | helm_install() { 11 | if test -f "${TF_CLI_CONFIG_FILE}"; then 12 | kubectl create secret generic terraformrc --from-file=credentials=${TF_CLI_CONFIG_FILE} 13 | else 14 | kubectl create secret generic terraformrc --from-file=credentials=${HOME}/.terraformrc 15 | fi 16 | 17 | kubectl create secret generic workspacesecrets 18 | 19 | local values="${BATS_TEST_DIRNAME}/values.yaml" 20 | if [ ! -f "${values}" ]; then 21 | touch $values 22 | fi 23 | 24 | helm install -f ${values} terraform --wait ${BATS_TEST_DIRNAME}/../.. 25 | } 26 | 27 | # helm_delete deletes the Terraform chart and all resources. 28 | helm_delete() { 29 | kubectl delete workspace terraform-terraform-test --ignore-not-found 30 | helm del terraform 31 | kubectl delete secret terraformrc 32 | kubectl delete secret workspacesecrets 33 | } 34 | 35 | # wait for a pod to be ready 36 | wait_for_ready() { 37 | POD_NAME=$1 38 | 39 | check() { 40 | # This requests the pod and checks whether the status is running 41 | # and the ready state is true. If so, it outputs the name. Otherwise 42 | # it outputs empty. Therefore, to check for success, check for nonzero 43 | # string length. 44 | kubectl get pods $1 -o json | \ 45 | jq -r 'select( 46 | .status.phase == "Running" and 47 | ([ .status.conditions[] | select(.type == "Ready" and .status == "True") ] | length) == 1 48 | ) | .metadata.namespace + "/" + .metadata.name' 49 | } 50 | 51 | for i in $(seq 30); do 52 | if [ -n "$(check ${POD_NAME})" ]; then 53 | echo "${POD_NAME} is ready." 54 | return 55 | fi 56 | 57 | echo "Waiting for ${POD_NAME} to be ready..." 58 | sleep 2 59 | done 60 | 61 | echo "${POD_NAME} never became ready." 62 | exit 1 63 | } -------------------------------------------------------------------------------- /test/acceptance/server.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "operator: default, comes up healthy" { 6 | helm_install 7 | wait_for_ready $(kubectl get pods --selector "component=sync-workspace" -o jsonpath="{.items[0].metadata.name}") 8 | 9 | helm test terraform 10 | 11 | # Clean up 12 | helm_delete 13 | } -------------------------------------------------------------------------------- /test/docker/Test.dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile installs all the dependencies necessary to run the unit and 2 | # acceptance tests. This image also contains gcloud so you can run tests 3 | # against a GKE cluster easily. 4 | # 5 | # This image has no automatic entrypoint. It is expected that you'll run 6 | # a script to configure kubectl, potentially install Helm, and run the tests 7 | # manually. This image only has the dependencies pre-installed. 8 | 9 | FROM docker.mirror.hashicorp.services/alpine/helm:3.2.0 10 | WORKDIR /root 11 | 12 | ENV BATS_VERSION "1.1.0" 13 | ENV TERRAFORM_VERSION "0.12.24" 14 | 15 | # base packages 16 | RUN apk update && apk add --no-cache --virtual .build-deps \ 17 | ca-certificates \ 18 | curl \ 19 | tar \ 20 | bash \ 21 | openssl \ 22 | python \ 23 | py-pip \ 24 | git \ 25 | jq 26 | 27 | # yq 28 | RUN pip install --no-cache-dir yq 29 | 30 | # gcloud 31 | RUN curl -OL https://dl.google.com/dl/cloudsdk/channels/rapid/install_google_cloud_sdk.bash && \ 32 | bash install_google_cloud_sdk.bash --disable-prompts --install-dir='/root/' && \ 33 | ln -s /root/google-cloud-sdk/bin/gcloud /usr/local/bin/gcloud 34 | 35 | # terraform 36 | RUN curl -sSL https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip -o /tmp/tf.zip \ 37 | && unzip /tmp/tf.zip \ 38 | && ln -s /root/terraform /usr/local/bin/terraform \ 39 | && rm /tmp/tf.zip 40 | 41 | # kubectl 42 | RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \ 43 | chmod +x ./kubectl && \ 44 | mv ./kubectl /usr/local/bin/kubectl 45 | 46 | # bats 47 | RUN curl -sSL https://github.com/bats-core/bats-core/archive/v${BATS_VERSION}.tar.gz -o /tmp/bats.tgz \ 48 | && tar -zxf /tmp/bats.tgz -C /tmp \ 49 | && /bin/bash /tmp/bats-core-${BATS_VERSION}/install.sh /usr/local \ 50 | && rm -rf /tmp/bats* 51 | -------------------------------------------------------------------------------- /test/module/random.tf: -------------------------------------------------------------------------------- 1 | variable "hellos" { 2 | type = object({ 3 | hello = string 4 | second_hello = string 5 | }) 6 | description = "list of hellos" 7 | } 8 | 9 | variable "some_key" { 10 | type = string 11 | description = "this is a some key" 12 | } 13 | 14 | resource "random_pet" "server" { 15 | keepers = { 16 | hello = var.hellos.hello 17 | secret_key = var.some_key 18 | } 19 | } 20 | 21 | resource "random_pet" "number_2" { 22 | keepers = { 23 | hello = var.hellos.second_hello 24 | } 25 | } 26 | 27 | output "pet" { 28 | value = random_pet.server.id 29 | } 30 | 31 | output "list_of_pets" { 32 | value = [random_pet.server.id, random_pet.number_2.id] 33 | } -------------------------------------------------------------------------------- /test/unit/_helpers.bash: -------------------------------------------------------------------------------- 1 | # chart_dir returns the directory for the chart 2 | chart_dir() { 3 | echo ${BATS_TEST_DIRNAME}/../.. 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/sync-workspace-deployment.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "syncWorkspace/Deployment: enabled by default" { 6 | cd `chart_dir` 7 | local actual=$(helm template \ 8 | --show-only templates/sync-workspace-deployment.yaml \ 9 | . | tee /dev/stderr | 10 | yq 'length > 0' | tee /dev/stderr) 11 | [ "${actual}" = "true" ] 12 | } 13 | 14 | @test "syncWorkspace/Deployment: enable with global.enabled false" { 15 | cd `chart_dir` 16 | local actual=$(helm template \ 17 | --show-only templates/sync-workspace-deployment.yaml \ 18 | --set 'global.enabled=false' \ 19 | --set 'syncWorkspace.enabled=true' \ 20 | . | tee /dev/stderr | 21 | yq 'length > 0' | tee /dev/stderr) 22 | [ "${actual}" = "true" ] 23 | } 24 | 25 | @test "syncWorkspace/Deployment: disable with syncWorkspace.enabled" { 26 | cd `chart_dir` 27 | local actual=$(helm template \ 28 | --show-only templates/sync-workspace-deployment.yaml \ 29 | --set 'syncWorkspace.enabled=false' \ 30 | . | tee /dev/stderr | 31 | yq 'length > 0' | tee /dev/stderr) 32 | [ "${actual}" = "false" ] 33 | } 34 | 35 | @test "syncWorkspace/Deployment: disable with global.enabled" { 36 | cd `chart_dir` 37 | local actual=$(helm template \ 38 | --show-only templates/sync-workspace-deployment.yaml \ 39 | --set 'global.enabled=false' \ 40 | . | tee /dev/stderr | 41 | yq 'length > 0' | tee /dev/stderr) 42 | [ "${actual}" = "false" ] 43 | } 44 | 45 | #-------------------------------------------------------------------- 46 | # image 47 | 48 | @test "syncWorkspace/Deployment: image defaults to global.imageK8S" { 49 | cd `chart_dir` 50 | local actual=$(helm template \ 51 | --show-only templates/sync-workspace-deployment.yaml \ 52 | --set 'global.imageK8S=bar' \ 53 | --set 'syncWorkspace.enabled=true' \ 54 | . | tee /dev/stderr | 55 | yq -r '.spec.template.spec.containers[0].image' | tee /dev/stderr) 56 | [ "${actual}" = "bar" ] 57 | } 58 | 59 | @test "syncWorkspace/Deployment: image can be overridden with syncWorkspace.image" { 60 | cd `chart_dir` 61 | local actual=$(helm template \ 62 | --show-only templates/sync-workspace-deployment.yaml \ 63 | --set 'global.imageK8S=foo' \ 64 | --set 'syncWorkspace.enabled=true' \ 65 | --set 'syncWorkspace.image=bar' \ 66 | . | tee /dev/stderr | 67 | yq -r '.spec.template.spec.containers[0].image' | tee /dev/stderr) 68 | [ "${actual}" = "bar" ] 69 | } 70 | 71 | #-------------------------------------------------------------------- 72 | # imagePullPolicy 73 | 74 | @test "syncWorkspace/Deployment: imagePullPolicy defaults to IfNotPresent" { 75 | cd `chart_dir` 76 | local actual=$(helm template \ 77 | --show-only templates/sync-workspace-deployment.yaml \ 78 | --set 'global.imageK8S=bar' \ 79 | --set 'syncWorkspace.enabled=true' \ 80 | . | tee /dev/stderr | 81 | yq -r '.spec.template.spec.containers[0].imagePullPolicy' | tee /dev/stderr) 82 | [ "${actual}" = "IfNotPresent" ] 83 | } 84 | 85 | @test "syncWorkspace/Deployment: imagePullPolicy can be overridden with syncWorkspace.imagePullPolicy" { 86 | cd `chart_dir` 87 | local actual=$(helm template \ 88 | --show-only templates/sync-workspace-deployment.yaml \ 89 | --set 'global.imageK8S=foo' \ 90 | --set 'syncWorkspace.enabled=true' \ 91 | --set 'syncWorkspace.imagePullPolicy=Never' \ 92 | . | tee /dev/stderr | 93 | yq -r '.spec.template.spec.containers[0].imagePullPolicy' | tee /dev/stderr) 94 | [ "${actual}" = "Never" ] 95 | } 96 | 97 | #-------------------------------------------------------------------- 98 | # tfe address 99 | 100 | @test "syncWorkspace/Deployment: tfe address defaults to blank string" { 101 | cd `chart_dir` 102 | local actual=$(helm template \ 103 | --show-only templates/sync-workspace-deployment.yaml \ 104 | --set 'syncWorkspace.enabled=true' \ 105 | . | tee /dev/stderr | 106 | yq -r '.spec.template.spec.containers[0].env[4].value' | tee /dev/stderr) 107 | [ "${actual}" = "" ] 108 | } 109 | 110 | @test "syncWorkspace/Deployment: tfe address defaults to tfe.local" { 111 | cd `chart_dir` 112 | local actual=$(helm template \ 113 | --show-only templates/sync-workspace-deployment.yaml \ 114 | --set 'syncWorkspace.enabled=true' \ 115 | --set 'syncWorkspace.tfeAddress=https://tfe.local' \ 116 | . | tee /dev/stderr | 117 | yq -r '.spec.template.spec.containers[0].env[4].value' | tee /dev/stderr) 118 | [ "${actual}" = "https://tfe.local" ] 119 | } 120 | 121 | #-------------------------------------------------------------------- 122 | # watch namespace 123 | 124 | @test "syncWorkspace/Deployment: watch namespace defaults to release namespace" { 125 | cd `chart_dir` 126 | local actual=$(helm template \ 127 | --show-only templates/sync-workspace-deployment.yaml \ 128 | --set 'syncWorkspace.enabled=true' \ 129 | . | tee /dev/stderr | 130 | yq -r '.spec.template.spec.containers[0].args | any(contains("--k8s-watch-namespace=default"))' | tee /dev/stderr) 131 | [ "${actual}" = "true" ] 132 | } 133 | 134 | @test "syncWorkspace/Deployment: watch namespace can be overridden" { 135 | cd `chart_dir` 136 | local actual=$(helm template \ 137 | --show-only templates/sync-workspace-deployment.yaml \ 138 | --set 'syncWorkspace.enabled=true' \ 139 | --set 'syncWorkspace.k8WatchNamespace=dev' \ 140 | . | tee /dev/stderr | 141 | yq -r '.spec.template.spec.containers[0].args | any(contains("-k8s-watch-namespace=dev"))' | tee /dev/stderr) 142 | [ "${actual}" = "true" ] 143 | } 144 | 145 | #-------------------------------------------------------------------- 146 | # terraform version 147 | 148 | @test "syncWorkspace/Deployment: terraform version defaults to operator-compiled" { 149 | cd `chart_dir` 150 | local actual=$(helm template \ 151 | --show-only templates/sync-workspace-deployment.yaml \ 152 | --set 'syncWorkspace.enabled=true' \ 153 | . | tee /dev/stderr | 154 | yq -r '.spec.template.spec.containers[0].env | any(contains("TF_VERSION"))' | tee /dev/stderr) 155 | [ "${actual}" = "" ] 156 | } 157 | 158 | @test "syncWorkspace/Deployment: terraform version is TF_VERSION" { 159 | cd `chart_dir` 160 | local actual=$(helm template \ 161 | --show-only templates/sync-workspace-deployment.yaml \ 162 | --set 'syncWorkspace.enabled=true' \ 163 | --set 'syncWorkspace.terraformVersion=0.11.1' \ 164 | . | tee /dev/stderr | 165 | yq -r '.spec.template.spec.containers[0].env[2].value == "0.11.1"' | tee /dev/stderr) 166 | [ "${actual}" = "true" ] 167 | } 168 | 169 | #-------------------------------------------------------------------- 170 | # serviceAccount 171 | 172 | @test "syncWorkspace/Deployment: serviceAccount set when sync enabled" { 173 | cd `chart_dir` 174 | local actual=$(helm template \ 175 | --show-only templates/sync-workspace-deployment.yaml \ 176 | --set 'syncWorkspace.enabled=true' \ 177 | . | tee /dev/stderr | 178 | yq '.spec.template.spec.serviceAccountName | contains("sync-workspace")' | tee /dev/stderr) 179 | [ "${actual}" = "true" ] 180 | } 181 | 182 | #-------------------------------------------------------------------- 183 | # secretsMounts 184 | 185 | @test "syncWorkspace/Deployment: add terraformrc secrets name" { 186 | cd `chart_dir` 187 | local actual=$(helm template \ 188 | --show-only templates/sync-workspace-deployment.yaml \ 189 | --set 'syncWorkspace.enabled=true' \ 190 | . | tee /dev/stderr | 191 | yq '.spec.template.spec.volumes[0].secret.secretName | contains("terraformrc")' | tee /dev/stderr) 192 | [ "${actual}" = "true" ] 193 | } 194 | 195 | @test "syncWorkspace/Deployment: add terraformrc secrets key" { 196 | cd `chart_dir` 197 | local actual=$(helm template \ 198 | --show-only templates/sync-workspace-deployment.yaml \ 199 | --set 'syncWorkspace.enabled=true' \ 200 | . | tee /dev/stderr | 201 | yq '.spec.template.spec.volumes[0].secret.items[0].key | contains("credentials")' | tee /dev/stderr) 202 | [ "${actual}" = "true" ] 203 | } 204 | 205 | @test "syncWorkspace/Deployment: terraformrc secrets variables can be overridden" { 206 | cd `chart_dir` 207 | local actual=$(helm template \ 208 | --show-only templates/sync-workspace-deployment.yaml \ 209 | --set 'syncWorkspace.enabled=true' \ 210 | --set 'syncWorkspace.terraformRC.secretName=terraformfile' \ 211 | . | tee /dev/stderr | 212 | yq '.spec.template.spec.volumes[0].secret.secretName | contains("terraformfile")' | tee /dev/stderr) 213 | [ "${actual}" = "true" ] 214 | } 215 | 216 | @test "syncWorkspace/Deployment: add workspace secrets variables" { 217 | cd `chart_dir` 218 | local actual=$(helm template \ 219 | --show-only templates/sync-workspace-deployment.yaml \ 220 | --set 'syncWorkspace.enabled=true' \ 221 | . | tee /dev/stderr | 222 | yq '.spec.template.spec.volumes[1].secret.secretName | contains("workspacesecrets")' | tee /dev/stderr) 223 | [ "${actual}" = "true" ] 224 | } 225 | 226 | @test "syncWorkspace/Deployment: workspace secrets variables can be overridden" { 227 | cd `chart_dir` 228 | local actual=$(helm template \ 229 | --show-only templates/sync-workspace-deployment.yaml \ 230 | --set 'syncWorkspace.enabled=true' \ 231 | --set 'syncWorkspace.sensitiveVariables.secretName=newsecrets' \ 232 | . | tee /dev/stderr | 233 | yq '.spec.template.spec.volumes[1].secret.secretName | contains("newsecrets")' | tee /dev/stderr) 234 | [ "${actual}" = "true" ] 235 | } 236 | -------------------------------------------------------------------------------- /test/unit/sync-workspace-role.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "syncWorkspace/Role: enabled by default" { 6 | cd `chart_dir` 7 | local actual=$(helm template \ 8 | --show-only templates/sync-workspace-role.yaml \ 9 | . | tee /dev/stderr | 10 | yq 'length > 0' | tee /dev/stderr) 11 | [ "${actual}" = "true" ] 12 | } 13 | 14 | @test "syncWorkspace/Role: disabled with global.enabled=false" { 15 | cd `chart_dir` 16 | local actual=$(helm template \ 17 | --show-only templates/sync-workspace-role.yaml 18 | --set 'global.enabled=false' \ 19 | . | tee /dev/stderr | 20 | yq 'length > 0' | tee /dev/stderr) 21 | [ "${actual}" = "" ] 22 | } 23 | 24 | @test "syncWorkspace/Role: disabled with sync disabled" { 25 | cd `chart_dir` 26 | local actual=$(helm template \ 27 | --show-only templates/sync-workspace-role.yaml 28 | --set 'syncWorkspace.enabled=false' \ 29 | . | tee /dev/stderr | 30 | yq 'length > 0' | tee /dev/stderr) 31 | [ "${actual}" = "" ] 32 | } 33 | 34 | @test "syncWorkspace/Role: enabled with sync enabled" { 35 | cd `chart_dir` 36 | local actual=$(helm template \ 37 | --show-only templates/sync-workspace-role.yaml \ 38 | --set 'syncWorkspace.enabled=true' \ 39 | . | tee /dev/stderr | 40 | yq 'length > 0' | tee /dev/stderr) 41 | [ "${actual}" = "true" ] 42 | } 43 | 44 | @test "syncWorkspace/Role: enabled with sync enabled and global.enabled=false" { 45 | cd `chart_dir` 46 | local actual=$(helm template \ 47 | --show-only templates/sync-workspace-role.yaml \ 48 | --set 'global.enabled=false' \ 49 | --set 'syncWorkspace.enabled=true' \ 50 | . | tee /dev/stderr | 51 | yq -s 'length > 0' | tee /dev/stderr) 52 | [ "${actual}" = "true" ] 53 | } -------------------------------------------------------------------------------- /test/unit/sync-workspace-rolebinding.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "syncWorkspace/RoleBinding: enabled by default" { 6 | cd `chart_dir` 7 | local actual=$(helm template \ 8 | --show-only templates/sync-workspace-rolebinding.yaml \ 9 | . | tee /dev/stderr | 10 | yq 'length > 0' | tee /dev/stderr) 11 | [ "${actual}" = "true" ] 12 | } 13 | 14 | @test "syncWorkspace/RoleBinding: disabled with global.enabled=false" { 15 | cd `chart_dir` 16 | local actual=$(helm template \ 17 | --show-only templates/sync-workspace-rolebinding.yaml \ 18 | --set 'global.enabled=false' \ 19 | . | tee /dev/stderr | 20 | yq 'length > 0' | tee /dev/stderr) 21 | [ "${actual}" = "" ] 22 | } 23 | 24 | @test "syncWorkspace/RoleBinding: disabled with sync disabled" { 25 | cd `chart_dir` 26 | local actual=$(helm template \ 27 | --show-only templates/sync-workspace-rolebinding.yaml \ 28 | --set 'syncWorkspace.enabled=false' \ 29 | . | tee /dev/stderr | 30 | yq 'length > 0' | tee /dev/stderr) 31 | [ "${actual}" = "" ] 32 | } 33 | 34 | @test "syncWorkspace/RoleBinding: enabled with sync enabled" { 35 | cd `chart_dir` 36 | local actual=$(helm template \ 37 | --show-only templates/sync-workspace-rolebinding.yaml \ 38 | --set 'syncWorkspace.enabled=true' \ 39 | . | tee /dev/stderr | 40 | yq -s 'length > 0' | tee /dev/stderr) 41 | [ "${actual}" = "true" ] 42 | } 43 | 44 | @test "syncWorkspace/RoleBinding: enabled with sync enabled and global.enabled=false" { 45 | cd `chart_dir` 46 | local actual=$(helm template \ 47 | --show-only templates/sync-workspace-rolebinding.yaml \ 48 | --set 'global.enabled=false' \ 49 | --set 'syncWorkspace.enabled=true' \ 50 | . | tee /dev/stderr | 51 | yq -s 'length > 0' | tee /dev/stderr) 52 | [ "${actual}" = "true" ] 53 | } 54 | -------------------------------------------------------------------------------- /test/unit/sync-workspace-serviceaccount.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "syncWorkspace/ServiceAccount: enabled by default" { 6 | cd `chart_dir` 7 | local actual=$(helm template \ 8 | --show-only templates/sync-workspace-serviceaccount.yaml \ 9 | . | tee /dev/stderr | 10 | yq 'length > 0' | tee /dev/stderr) 11 | [ "${actual}" = "true" ] 12 | } 13 | 14 | @test "syncWorkspace/ServiceAccount: disabled with global.enabled=false" { 15 | cd `chart_dir` 16 | local actual=$(helm template \ 17 | --show-only templates/sync-workspace-serviceaccount.yaml \ 18 | --set 'global.enabled=false' \ 19 | . | tee /dev/stderr | 20 | yq 'length > 0' | tee /dev/stderr) 21 | [ "${actual}" = "" ] 22 | } 23 | 24 | @test "syncWorkspace/ServiceAccount: disabled with sync disabled" { 25 | cd `chart_dir` 26 | local actual=$(helm template \ 27 | --show-only templates/sync-workspace-serviceaccount.yaml \ 28 | --set 'syncWorkspace.enabled=false' \ 29 | . | tee /dev/stderr | 30 | yq 'length > 0' | tee /dev/stderr) 31 | [ "${actual}" = "" ] 32 | } 33 | 34 | @test "syncWorkspace/ServiceAccount: enabled with sync enabled" { 35 | cd `chart_dir` 36 | local actual=$(helm template \ 37 | --show-only templates/sync-workspace-serviceaccount.yaml \ 38 | --set 'syncWorkspace.enabled=true' \ 39 | . | tee /dev/stderr | 40 | yq 'length > 0' | tee /dev/stderr) 41 | [ "${actual}" = "true" ] 42 | } 43 | 44 | @test "syncWorkspace/ServiceAccount: enabled with sync enabled and global.enabled=false" { 45 | cd `chart_dir` 46 | local actual=$(helm template \ 47 | --show-only templates/sync-workspace-serviceaccount.yaml \ 48 | --set 'global.enabled=false' \ 49 | --set 'syncWorkspace.enabled=true' \ 50 | . | tee /dev/stderr | 51 | yq -s 'length > 0' | tee /dev/stderr) 52 | [ "${actual}" = "true" ] 53 | } 54 | -------------------------------------------------------------------------------- /test/unit/test-configmap.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "testConfigMap/ConfigMap: enabled by default" { 6 | cd `chart_dir` 7 | local actual=$(helm template \ 8 | --show-only templates/tests/test-configmap.yaml \ 9 | . | tee /dev/stderr | 10 | yq 'length > 0' | tee /dev/stderr) 11 | [ "${actual}" = "true" ] 12 | } 13 | 14 | @test "testConfigMap/ConfigMap: disabled when tests.enabled=false" { 15 | cd `chart_dir` 16 | local actual=$(helm template \ 17 | --show-only templates/tests/test-configmap.yaml \ 18 | --set 'tests.enabled=false' \ 19 | . | tee /dev/stderr | 20 | yq 'length > 0' | tee /dev/stderr) 21 | [ "${actual}" = "" ] 22 | } 23 | 24 | @test "testConfigMap/ConfigMap: contains terraform backend configuration" { 25 | cd `chart_dir` 26 | local actual=$(helm template operator \ 27 | --show-only templates/tests/test-configmap.yaml \ 28 | . | tee /dev/stderr | 29 | yq -r '.data.backend | contains("organization = tf-operator") ' | tee /dev/stderr) 30 | [ "${actual}" = "true" ] 31 | } -------------------------------------------------------------------------------- /test/unit/test-runner.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "testRunner/Pod: enabled by default" { 6 | cd `chart_dir` 7 | local actual=$(helm template \ 8 | --show-only templates/tests/test-runner.yaml \ 9 | . | tee /dev/stderr | 10 | yq 'length > 0' | tee /dev/stderr) 11 | [ "${actual}" = "true" ] 12 | } 13 | 14 | @test "testRunner/Pod: disabled when tests.enabled=false" { 15 | cd `chart_dir` 16 | local actual=$(helm template \ 17 | --show-only templates/tests/test-runner.yaml \ 18 | --set 'tests.enabled=false' \ 19 | . | tee /dev/stderr | 20 | yq 'length > 0' | tee /dev/stderr) 21 | [ "${actual}" = "" ] 22 | } -------------------------------------------------------------------------------- /test/unit/test-workspace.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | @test "testWorkspace/Workspace: enabled by default" { 6 | cd `chart_dir` 7 | local actual=$(helm template \ 8 | --show-only templates/tests/test-workspace.yaml \ 9 | . | tee /dev/stderr | 10 | yq 'length > 0' | tee /dev/stderr) 11 | [ "${actual}" = "true" ] 12 | } 13 | 14 | @test "testWorkspace/Workspace: disabled when tests.enabled=false" { 15 | cd `chart_dir` 16 | local actual=$(helm template \ 17 | --show-only templates/tests/test-workspace.yaml \ 18 | --set 'tests.enabled=false' \ 19 | . | tee /dev/stderr | 20 | yq 'length > 0' | tee /dev/stderr) 21 | [ "${actual}" = "" ] 22 | } 23 | 24 | @test "testWorkspace/Workspace: module source set to git by default" { 25 | cd `chart_dir` 26 | local actual=$(helm template \ 27 | --show-only templates/tests/test-workspace.yaml \ 28 | . | tee /dev/stderr | 29 | yq -r '.spec.module.source' | tee /dev/stderr) 30 | [ "${actual}" = "git::https://github.com/hashicorp/terraform-helm.git//test/module" ] 31 | } 32 | 33 | @test "testWorkspace/Workspace: module source can be overridden" { 34 | cd `chart_dir` 35 | local actual=$(helm template \ 36 | --show-only templates/tests/test-workspace.yaml \ 37 | --set 'tests.moduleSource=hello/random' \ 38 | . | tee /dev/stderr | 39 | yq -r '.spec.module.source' | tee /dev/stderr) 40 | [ "${actual}" = "hello/random" ] 41 | } 42 | 43 | @test "testWorkspace/Workspace: organization set by default" { 44 | cd `chart_dir` 45 | local actual=$(helm template \ 46 | --show-only templates/tests/test-workspace.yaml \ 47 | . | tee /dev/stderr | 48 | yq -r '.spec.organization' | tee /dev/stderr) 49 | [ "${actual}" = "tf-operator" ] 50 | } 51 | 52 | @test "testWorkspace/Workspace: organization can be overridden" { 53 | cd `chart_dir` 54 | local actual=$(helm template \ 55 | --show-only templates/tests/test-workspace.yaml \ 56 | --set 'tests.organization=demo' \ 57 | . | tee /dev/stderr | 58 | yq -r '.spec.organization' | tee /dev/stderr) 59 | [ "${actual}" = "demo" ] 60 | } -------------------------------------------------------------------------------- /values.dev.yaml: -------------------------------------------------------------------------------- 1 | # Parameters used for testing operator in development 2 | # run with `helm install -f values.yml -f values.dev.yaml .` 3 | 4 | global: 5 | imageK8S: "terraform-k8s-dev:latest" 6 | imagePullPolicy: "Never" 7 | 8 | syncWorkspace: 9 | terraformVersion: 0.12.25 -------------------------------------------------------------------------------- /values.yaml: -------------------------------------------------------------------------------- 1 | # Available parameters and their default values for the Terraform chart. 2 | 3 | # global holds values that affect multiple components of the chart. 4 | global: 5 | # enabled is the master enabled/disabled setting. 6 | # If true, all Terraform Kubernetes integrations will be enabled. 7 | # If false, no components will be installed by default and per-component 8 | # opt-in is required, such as by setting `syncWorkspace.enabled` to true. 9 | enabled: true 10 | 11 | # imageK8S is the name (and tag) of the terraform-k8s Docker image that 12 | # is used for functionality such as workspace sync. This can be overridden 13 | # per component. 14 | imageK8S: "hashicorp/terraform-k8s:1.1.2" 15 | 16 | # syncWorkspace will run the workspace sync process to sync K8S Workspaces with 17 | # Terraform Cloud workspaces. 18 | # 19 | # This process will deploy a Workspace Custom Resource Definition and 20 | # Terraform Cloud Workspace Operator to the Kubernetes cluster. It requires 21 | # a Kubernetes secret for Terraform Cloud access and sensitive variables. 22 | syncWorkspace: 23 | # True if you want to enable the workspace sync. Set to "-" to inherit from 24 | # global.enabled. 25 | enabled: "-" 26 | image: null 27 | 28 | # If true it disables certificate validation for all outbound HTTPS connections 29 | # the operator has to do. It can be used when connecting to TFE instances with 30 | # self-signed certificates 31 | insecure: false 32 | 33 | # k8WatchNamespace is the Kubernetes namespace to watch for workspace 34 | # changes and sync to Terraform Cloud. If this is not set then it will default 35 | # to the release namespace 36 | k8WatchNamespace: null 37 | 38 | # terraformVersion describes the version of Terraform to use for each workspace. 39 | # If this is not set then it will default to the latest version of Terraform 40 | # compiled with the operator. 41 | terraformVersion: latest 42 | # tfeAddress denotes the address in the form of https://tfe.local for 43 | # a Terraform Enterprise instance. If this is not set then it will default 44 | # to Terraform Cloud (https://app.terraform.io) 45 | tfeAddress: null 46 | 47 | # WARNING: logLevel not supported by terraform-k8s:1.0.0 version, or earlier. 48 | # Uncomment to configure log verbosity level. One of "debug", "info", or "error". 49 | # Defaults to "debug". 50 | # logLevel: error 51 | 52 | # Name and key of Kubernetes secret containing the Terraform CLI Configuration 53 | # Must have Terraform Cloud Team API Token 54 | terraformRC: 55 | secretName: terraformrc 56 | secretKey: credentials 57 | 58 | # Name of Kubernetes secret containing keys and values of sensitive variables 59 | sensitiveVariables: 60 | secretName: workspacesecrets 61 | 62 | # Control whether a test Pod manifest is generated when running helm template. 63 | # When using helm install, the test Pod is not submitted to the cluster so this 64 | # is only useful when running helm template. 65 | tests: 66 | enabled: true 67 | organization: tf-operator 68 | # moduleSource defaults to this repository. 69 | # moduleSource: git::https://github.com/hashicorp/terraform-helm.git//test/module 70 | 71 | # If the TFE instance the operator talks to is using a private CA then CACerts 72 | # can be used to install a custom CA bundle in the operator's container that 73 | # will allow it to communicate with TFE. 74 | # 75 | # CACerts can be set like this: 76 | # helm install --set-file CACerts=/path/to/certs.pem 77 | CACerts: "" 78 | --------------------------------------------------------------------------------