├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── renovate.json5 ├── stale.yml └── workflows │ ├── ci.yml │ ├── publish-provider-packages.yaml │ ├── stale.yml │ ├── tag.yaml │ └── uptest-trigger.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── OWNERS.md ├── README.md ├── apis ├── cluster │ ├── terraform.go │ └── v1beta1 │ │ ├── doc.go │ │ ├── register.go │ │ ├── types.go │ │ ├── workspace_types.go │ │ ├── zz_generated.deepcopy.go │ │ ├── zz_generated.managed.go │ │ ├── zz_generated.managedlist.go │ │ ├── zz_generated.pc.go │ │ ├── zz_generated.pcu.go │ │ └── zz_generated.pculist.go └── namespaced │ ├── terraform.go │ └── v1beta1 │ ├── doc.go │ ├── register.go │ ├── types.go │ ├── workspace_types.go │ ├── zz_generated.deepcopy.go │ ├── zz_generated.managed.go │ ├── zz_generated.managedlist.go │ ├── zz_generated.pc.go │ ├── zz_generated.pcu.go │ └── zz_generated.pculist.go ├── catalog-info.yaml ├── cluster ├── images │ └── provider-terraform │ │ ├── .gitconfig │ │ ├── Dockerfile │ │ └── Makefile └── test │ └── setup.sh ├── cmd └── provider │ └── main.go ├── docs └── monolith │ ├── Configuration.md │ ├── Quickstart.md │ ├── Troubleshooting.md │ └── index.json ├── examples ├── cluster │ ├── .terraformrc │ ├── aws-eks-irsa-setup.yaml │ ├── creationblocker │ │ ├── README.md │ │ ├── composition.yaml │ │ ├── definition.yaml │ │ ├── xsubnetblocking.yaml │ │ └── xsubnetnotblocking.yaml │ ├── environment │ │ ├── envconfigmap.yaml │ │ ├── envsecret.yaml │ │ └── workspace-inline-env-aws.yaml │ ├── importer │ │ ├── README.md │ │ ├── composition.yaml │ │ ├── definition.yaml │ │ └── xsubnet.yaml │ ├── install.yaml │ ├── observe-only-composition │ │ ├── README.md │ │ ├── composition.yaml │ │ ├── definition.yaml │ │ └── xsubnet.yaml │ ├── providerconfig-aws.yaml │ ├── providerconfig-azure.yaml │ ├── providerconfig-backend-file.yaml │ ├── providerconfig-terraformrc.yaml │ ├── providerconfig.yaml │ ├── transition │ │ ├── 00-mr-tf-workspace │ │ │ ├── workspace-inline.yaml │ │ │ └── workspace-remote.yaml │ │ ├── 01-composition-tf-only │ │ │ ├── composition.yaml │ │ │ └── subnet-tf.yaml │ │ ├── 02-composition-tf-and-native │ │ │ ├── composition.yaml │ │ │ └── subnet-mixed.yaml │ │ ├── 03-composition-native-only │ │ │ ├── composition.yaml │ │ │ └── subnet-native.yaml │ │ ├── README.md │ │ └── definition.yaml │ ├── workspace-enable-logging.yaml │ ├── workspace-inline-aws.yaml │ ├── workspace-inline.yaml │ ├── workspace-random-generator.yaml │ └── workspace-remote.yaml └── namespaced │ ├── clusterproviderconfig-aws.yaml │ ├── clusterproviderconfig.yaml │ ├── providerconfig.yaml │ └── workspace-inline-aws.yaml ├── generate └── generate.go ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal ├── bootcheck │ └── default.go ├── clients │ ├── client.go │ └── interfaces.go ├── controller │ ├── cluster │ │ ├── config │ │ │ └── config.go │ │ ├── doc.go │ │ ├── terraform.go │ │ └── workspace │ │ │ ├── workspace.go │ │ │ └── workspace_test.go │ └── namespaced │ │ ├── config │ │ └── config.go │ │ ├── doc.go │ │ ├── terraform.go │ │ └── workspace │ │ ├── workspace.go │ │ └── workspace_test.go ├── features │ └── features.go ├── terraform │ ├── terraform.go │ ├── terraform_harness_test.go │ ├── terraform_test.go │ └── testdata │ │ ├── invalidmodule │ │ └── main.tf │ │ ├── nullmodule │ │ ├── main.tf │ │ └── terraform.tfstate │ │ ├── outputmodule │ │ ├── main.tf │ │ └── terraform.tfstate │ │ └── validmodule │ │ └── main.tf └── workdir │ ├── workdir.go │ └── workdir_test.go ├── package ├── crds │ ├── tf.m.upbound.io_clusterproviderconfigs.yaml │ ├── tf.m.upbound.io_providerconfigs.yaml │ ├── tf.m.upbound.io_providerconfigusages.yaml │ ├── tf.m.upbound.io_workspaces.yaml │ ├── tf.upbound.io_providerconfigs.yaml │ ├── tf.upbound.io_providerconfigusages.yaml │ └── tf.upbound.io_workspaces.yaml └── crossplane.yaml └── scripts └── check-examples.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Help us diagnose and fix bugs in this provider 4 | labels: bug,needs:triage 5 | title: 6 | --- 7 | 12 | 13 | ### What happened? 14 | 18 | 19 | ### How can we reproduce it? 20 | 25 | 26 | ### What environment did it happen in? 27 | 28 | * Crossplane Version: 29 | * Provider Version: 30 | * Kubernetes Version: 31 | * Kubernetes Distribution: 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Help us make this provider more useful 4 | labels: enhancement,needs:triage 5 | --- 6 | 7 | 13 | 14 | ### What problem are you facing? 15 | 16 | 21 | 22 | ### What could help solve your problem? 23 | 24 | 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### Description of your changes 8 | 9 | 17 | 18 | Fixes # 19 | 20 | I have: 21 | 22 | - [ ] Read and followed Crossplane's [contribution process]. 23 | - [ ] Run `make reviewable` to ensure this PR is ready for review. 24 | 25 | ### How has this code been tested 26 | 27 | 32 | 33 | [contribution process]: https://git.io/fj2m9 34 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | 'helpers:pinGitHubActionDigests', 6 | ':semanticCommits', 7 | ], 8 | rebaseWhen: 'conflicted', 9 | prConcurrentLimit: 5, 10 | baseBranches: [ 11 | 'main', 12 | ], 13 | ignorePaths: [ 14 | 'design/**', 15 | ], 16 | postUpdateOptions: [ 17 | 'gomodTidy', 18 | ], 19 | labels: [ 20 | 'automated', 21 | ], 22 | customManagers: [ 23 | { 24 | customType: 'regex', 25 | description: 'Bump Go version used in workflows', 26 | fileMatch: [ 27 | '^\\.github\\/workflows\\/[^/]+\\.ya?ml$', 28 | ], 29 | matchStrings: [ 30 | "GO_VERSION: '(?.*?)'\\n", 31 | ], 32 | datasourceTemplate: 'golang-version', 33 | depNameTemplate: 'golang', 34 | }, 35 | { 36 | customType: 'regex', 37 | description: 'Bump golangci-lint version in workflows and the Makefile', 38 | fileMatch: [ 39 | '^\\.github\\/workflows\\/[^/]+\\.ya?ml$', 40 | '^Makefile$', 41 | ], 42 | matchStrings: [ 43 | "GOLANGCI_VERSION: 'v(?.*?)'\\n", 44 | 'GOLANGCILINT_VERSION = (?.*?)\\n', 45 | ], 46 | datasourceTemplate: 'github-tags', 47 | depNameTemplate: 'golangci/golangci-lint', 48 | extractVersionTemplate: '^v(?.*)$', 49 | }, 50 | { 51 | customType: 'regex', 52 | description: 'Bump helm version in the Makefile', 53 | fileMatch: [ 54 | '^Makefile$', 55 | ], 56 | matchStrings: [ 57 | 'HELM3_VERSION = (?.*?)\\n', 58 | ], 59 | datasourceTemplate: 'github-tags', 60 | depNameTemplate: 'helm/helm', 61 | }, 62 | { 63 | customType: 'regex', 64 | description: 'Bump kind version in the Makefile', 65 | fileMatch: [ 66 | '^Makefile$', 67 | ], 68 | matchStrings: [ 69 | 'KIND_VERSION = (?.*?)\\n', 70 | ], 71 | datasourceTemplate: 'github-tags', 72 | depNameTemplate: 'kubernetes-sigs/kind', 73 | }, 74 | ], 75 | vulnerabilityAlerts: { 76 | enabled: true, 77 | }, 78 | osvVulnerabilityAlerts: true, 79 | packageRules: [ 80 | { 81 | description: 'Only get Docker image updates every 2 weeks to reduce noise', 82 | matchDatasources: [ 83 | 'docker', 84 | ], 85 | schedule: [ 86 | 'every 2 week on monday', 87 | ], 88 | enabled: true, 89 | }, 90 | { 91 | description: 'Ignore k8s.io/client-go older versions, they switched to semantic version and old tags are still available in the repo', 92 | matchDatasources: [ 93 | 'go', 94 | ], 95 | matchDepNames: [ 96 | 'k8s.io/client-go', 97 | ], 98 | allowedVersions: '<1.0', 99 | }, 100 | { 101 | description: 'Ignore k8s dependencies, should be updated on crossplane-runtime', 102 | matchDatasources: [ 103 | 'go', 104 | ], 105 | enabled: false, 106 | matchPackageNames: [ 107 | 'k8s.io{/,}**', 108 | 'sigs.k8s.io{/,}**', 109 | ], 110 | }, 111 | { 112 | description: 'Only get dependency digest updates every month to reduce noise, except crossplane-runtime', 113 | matchDatasources: [ 114 | 'go', 115 | ], 116 | matchUpdateTypes: [ 117 | 'digest', 118 | ], 119 | extends: [ 120 | 'schedule:monthly', 121 | ], 122 | matchPackageNames: [ 123 | '!github.com/crossplane/crossplane-runtime', 124 | ], 125 | }, 126 | { 127 | description: "Ignore oss-fuzz, it's not using tags, we'll stick to master", 128 | matchDepTypes: [ 129 | 'action', 130 | ], 131 | matchDepNames: [ 132 | 'google/oss-fuzz', 133 | ], 134 | enabled: false, 135 | }, 136 | ], 137 | } 138 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before a stale Issue or Pull Request is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 11 | exemptLabels: 12 | - security 13 | 14 | # Set to true to ignore issues in a project (defaults to false) 15 | exemptProjects: false 16 | 17 | # Set to true to ignore issues in a milestone (defaults to false) 18 | exemptMilestones: false 19 | 20 | # Label to use when marking as stale 21 | staleLabel: wontfix 22 | 23 | # Comment to post when marking as stale. Set to `false` to disable 24 | markComment: > 25 | This issue has been automatically marked as stale because it has not had 26 | recent activity. It will be closed if no further activity occurs. Thank you 27 | for your contributions. 28 | 29 | # Comment to post when closing a stale Issue or Pull Request. 30 | closeComment: > 31 | This issue has been automatically closed due to inactivity. Please re-open 32 | if this still requires investigation. 33 | 34 | # Limit the number of actions per hour, from 1-30. Default is 30 35 | limitPerRun: 30 36 | 37 | # Limit to only `issues` or `pulls` 38 | only: issues 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 The Crossplane Authors 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: CI 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | - release-* 12 | pull_request: {} 13 | workflow_dispatch: {} 14 | 15 | env: 16 | GO_VERSION: "1.22.12" 17 | 18 | jobs: 19 | detect-noop: 20 | runs-on: ubuntu-latest 21 | outputs: 22 | noop: ${{ steps.noop.outputs.should_skip }} 23 | steps: 24 | - name: Detect No-op Changes 25 | id: noop 26 | uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 27 | with: 28 | paths_ignore: '["**.md", "**.png", "**.jpg"]' 29 | do_not_skip: '["workflow_dispatch", "schedule", "push"]' 30 | 31 | report-breaking-changes: 32 | runs-on: ubuntu-latest 33 | needs: detect-noop 34 | if: needs.detect-noop.outputs.noop != 'true' 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 38 | with: 39 | submodules: true 40 | 41 | - name: Get modified CRDs 42 | id: modified-crds 43 | uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 (breaks the 'Report native schema version changes' step on newer versions) 44 | with: 45 | files: | 46 | package/crds/** 47 | 48 | - name: Report breaking CRD OpenAPI v3 schema changes 49 | if: steps.modified-crds.outputs.any_changed == 'true' 50 | env: 51 | MODIFIED_CRD_LIST: ${{ steps.modified-crds.outputs.all_changed_files }} 52 | run: | 53 | make crddiff 54 | 55 | lint: 56 | runs-on: ubuntu-latest 57 | needs: detect-noop 58 | if: needs.detect-noop.outputs.noop != 'true' 59 | steps: 60 | - name: Cleanup Disk 61 | uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 62 | with: 63 | large-packages: false 64 | swap-storage: false 65 | 66 | - name: Checkout 67 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 68 | with: 69 | submodules: true 70 | 71 | - name: Setup Go 72 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 73 | with: 74 | go-version: ${{ env.GO_VERSION }} 75 | 76 | - name: Find the Analysis Cache 77 | id: analysis_cache 78 | run: | 79 | echo "analysis_cache=$HOME/.cache/golangci-lint" >> $GITHUB_OUTPUT && \ 80 | echo "analysis_cache_key=$(make go.lint.analysiskey)" >> $GITHUB_OUTPUT && \ 81 | echo "analysis_cache_key_int=$(make go.lint.analysiskey-interval)" >> $GITHUB_OUTPUT 82 | 83 | - name: Cache Linter Analysis 84 | uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 85 | id: cache-analysis 86 | with: 87 | path: ${{ steps.analysis_cache.outputs.analysis_cache }} 88 | key: ${{ steps.analysis_cache.outputs.analysis_cache_key }} 89 | restore-keys: | 90 | ${{ steps.analysis_cache.outputs.analysis_cache_key_int }} 91 | 92 | - name: Vendor Dependencies 93 | run: make vendor vendor.check 94 | 95 | - name: Lint 96 | env: 97 | GOLANGCI_LINT_CACHE: ${{ steps.go_cache.outputs.analysis_cache }} 98 | SKIP_LINTER_ANALYSIS: false 99 | RUN_BUILDTAGGER: true 100 | GOGC: "50" 101 | run: make lint 102 | 103 | check-diff: 104 | runs-on: ubuntu-latest 105 | needs: detect-noop 106 | if: needs.detect-noop.outputs.noop != 'true' 107 | steps: 108 | - name: Cleanup Disk 109 | uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 110 | with: 111 | large-packages: false 112 | swap-storage: false 113 | 114 | - name: Checkout 115 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 116 | with: 117 | submodules: true 118 | 119 | - name: Setup Go 120 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 121 | with: 122 | go-version: ${{ env.GO_VERSION }} 123 | 124 | - name: Install goimports 125 | run: go install golang.org/x/tools/cmd/goimports 126 | 127 | - name: Vendor Dependencies 128 | run: make vendor vendor.check 129 | 130 | - name: Check Diff 131 | id: check-diff 132 | run: | 133 | mkdir _output 134 | make check-diff 135 | env: 136 | # check-diff depends on the generate Make target, and we would like 137 | # to save a skipped resource list 138 | SKIPPED_RESOURCES_CSV: ../_output/skipped_resources.csv 139 | 140 | - name: Show diff 141 | if: failure() && steps.check-diff.outcome == 'failure' 142 | run: git diff 143 | 144 | - name: Publish skipped resources CSV to Github 145 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 146 | with: 147 | name: skipped_resources 148 | path: _output/skipped_resources.csv 149 | 150 | unit-tests: 151 | runs-on: ubuntu-latest 152 | needs: detect-noop 153 | if: needs.detect-noop.outputs.noop != 'true' 154 | steps: 155 | - name: Cleanup Disk 156 | uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 157 | with: 158 | large-packages: false 159 | swap-storage: false 160 | 161 | - name: Checkout 162 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 163 | with: 164 | submodules: true 165 | 166 | - name: Fetch History 167 | run: git fetch --prune --unshallow 168 | 169 | - name: Setup Go 170 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 171 | with: 172 | go-version: ${{ env.GO_VERSION }} 173 | 174 | - name: Vendor Dependencies 175 | run: make vendor vendor.check 176 | 177 | - name: Run Unit Tests 178 | run: make -j2 test 179 | 180 | local-deploy: 181 | runs-on: ubuntu-latest 182 | needs: detect-noop 183 | if: needs.detect-noop.outputs.noop != 'true' 184 | steps: 185 | - name: Cleanup Disk 186 | uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 187 | with: 188 | large-packages: false 189 | swap-storage: false 190 | 191 | - name: Checkout 192 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 193 | with: 194 | submodules: true 195 | 196 | - name: Fetch History 197 | run: git fetch --prune --unshallow 198 | 199 | - name: Setup Go 200 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 201 | with: 202 | go-version: ${{ env.GO_VERSION }} 203 | 204 | - name: Vendor Dependencies 205 | run: make vendor vendor.check 206 | 207 | - name: Deploying locally built provider package 208 | run: make local-deploy 209 | 210 | check-examples: 211 | runs-on: ubuntu-latest 212 | needs: detect-noop 213 | if: ${{ needs.detect-noop.outputs.noop != 'true' }} 214 | 215 | steps: 216 | - name: Checkout 217 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 218 | with: 219 | submodules: true 220 | 221 | - name: Check Example Manifests 222 | run: | 223 | ./scripts/check-examples.py package/crds examples -------------------------------------------------------------------------------- /.github/workflows/publish-provider-packages.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Provider Package 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version string to use while publishing the package (e.g. v1.0.0-alpha.1)" 8 | default: '' 9 | required: false 10 | go-version: 11 | description: 'Go version to use if building needs to be done' 12 | default: '1.23' 13 | required: false 14 | 15 | jobs: 16 | publish-provider-package: 17 | uses: crossplane-contrib/provider-workflows/.github/workflows/publish-provider-non-family.yml@main 18 | with: 19 | repository: provider-terraform 20 | version: ${{ github.event.inputs.version }} 21 | go-version: ${{ github.event.inputs.go-version }} 22 | cleanup-disk: true 23 | secrets: 24 | GHCR_PAT: ${{ secrets.GITHUB_TOKEN }} 25 | XPKG_MIRROR_TOKEN: ${{ secrets.XPKG_TOKEN }} 26 | XPKG_MIRROR_ACCESS_ID: ${{ secrets.XPKG_ACCESS_ID }} -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale Issues and PRs 2 | on: 3 | schedule: 4 | # Process new stale issues once a day. Folks can use /fresh for a fast un-stale 5 | # per the commands workflow. Run at 4:15 mostly as a somewhat unique time to 6 | # help correlate any issues with this workflow. 7 | - cron: '15 4 * * *' 8 | workflow_dispatch: {} 9 | 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | 14 | jobs: 15 | stale: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 19 | with: 20 | # This action uses ~2 operations per stale issue per run to determine 21 | # whether it's still stale. It also uses 2-3 operations to mark an issue 22 | # stale or not. During steady state (no issues to mark stale, check, or 23 | # close) we seem to use less than 10 operations with ~350 issues and PRs 24 | # open. 25 | # 26 | # Our hourly rate-limit budget for all workflows that use GITHUB_TOKEN 27 | # is 1,000 requests per the below docs. 28 | # https://docs.github.com/en/rest/overview/resources-in-the-rest-api#requests-from-github-actions 29 | operations-per-run: 100 30 | # For issues: post a "warning" message after 90 days, then close if another 14 days pass without a response. 31 | days-before-issue-stale: 90 32 | days-before-issue-close: 14 33 | stale-issue-label: stale 34 | exempt-issue-labels: exempt-from-stale 35 | stale-issue-message: > 36 | This provider repo does not have enough maintainers to address every 37 | issue. Since there has been no activity in the last 90 days it is now marked 38 | as `stale`. It will be closed in 14 days if no further activity occurs. 39 | Leaving a comment **starting with** `/fresh` will mark this issue as not 40 | stale. 41 | close-issue-message: > 42 | This issue is being closed since there has been no activity for 14 days 43 | since marking it as `stale`. If you still need help, feel free to 44 | comment or reopen the issue! 45 | # For PRs: post a "warning" message after 90 days, then close if another 14 days pass without a response. 46 | days-before-pr-stale: 90 47 | days-before-pr-close: 14 48 | stale-pr-label: stale 49 | exempt-pr-labels: exempt-from-stale 50 | stale-pr-message: > 51 | This provider repo does not have enough maintainers to address every 52 | pull request. Since there has been no activity in the last 90 days it is 53 | now marked as `stale`. It will be closed in 14 days if no further activity 54 | occurs. Leaving a comment **starting with** `/fresh` will mark this issue 55 | as not stale. 56 | close-pr-message: > 57 | This pull request is being closed since there has been no activity for 14 days 58 | since marking it as `stale`. If you're still working on this, feel free 59 | to reopen the PR or create a new one! 60 | -------------------------------------------------------------------------------- /.github/workflows/tag.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 The Crossplane Authors 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: Tag 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | version: 11 | description: 'Release version (e.g. v0.1.0)' 12 | required: true 13 | message: 14 | description: 'Tag message' 15 | required: true 16 | 17 | jobs: 18 | create-tag: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 24 | 25 | - name: Create Tag 26 | uses: negz/create-tag@39bae1e0932567a58c20dea5a1a0d18358503320 # v1 27 | with: 28 | version: ${{ github.event.inputs.version }} 29 | message: ${{ github.event.inputs.message }} 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/uptest-trigger.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 The Crossplane Authors 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: End to End Testing 6 | 7 | on: 8 | issue_comment: 9 | types: [created] 10 | 11 | env: 12 | GO_VERSION: "1.23.6" 13 | 14 | jobs: 15 | check-permissions: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | permission: ${{ steps.check-permissions.outputs.permission }} 19 | steps: 20 | - name: Get Commenter Permissions 21 | id: check-permissions 22 | run: | 23 | echo "Trigger keyword: '/test-examples'" 24 | echo "Go version: ${{ env.GO_VERSION }}" 25 | 26 | REPO=${{ github.repository }} 27 | COMMENTER=${{ github.event.comment.user.login }} 28 | 29 | # Fetch the commenter's repo-level permission grant 30 | GRANTED=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 31 | -H "Accept: application/vnd.github.v3+json" \ 32 | "https://api.github.com/repos/$REPO/collaborators/$COMMENTER/permission" | jq -r .permission) 33 | 34 | # Make it accessible in the workflow via a job output -- cannot use env 35 | echo "User $COMMENTER has $GRANTED permissions" 36 | echo "permission=$GRANTED" >> "$GITHUB_OUTPUT" 37 | 38 | get-example-list: 39 | needs: check-permissions 40 | if: ${{ (needs.check-permissions.outputs.permission == 'admin' || needs.check-permissions.outputs.permission == 'write') && github.event.issue.pull_request != null && contains(github.event.comment.body, '/test-examples')}} 41 | runs-on: ubuntu-latest 42 | outputs: 43 | example_list: ${{ steps.get-example-list-name.outputs.example-list }} 44 | example_hash: ${{ steps.get-example-list-name.outputs.example-hash }} 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 49 | with: 50 | submodules: true 51 | 52 | - name: Checkout PR 53 | id: checkout-pr 54 | env: 55 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | run: | 57 | gh pr checkout ${{ github.event.issue.number }} 58 | git submodule update --init --recursive 59 | OUTPUT=$(git log -1 --format='%H') 60 | echo "commit-sha=$OUTPUT" >> $GITHUB_OUTPUT 61 | 62 | - name: Prepare The Example List 63 | env: 64 | COMMENT: ${{ github.event.comment.body }} 65 | id: get-example-list-name 66 | run: | 67 | PATHS=$(echo $COMMENT | sed 's/^.*\/test-examples="//g' | cut -d '"' -f 1 | sed 's/,/ /g') 68 | EXAMPLE_LIST="" 69 | for P in $PATHS; do EXAMPLE_LIST="${EXAMPLE_LIST},$(find $P -name '*.yaml' | tr '\n' ',')"; done 70 | 71 | sudo apt-get -y install coreutils 72 | COUNT=$(echo ${EXAMPLE_LIST:1} | grep -o ".yaml" | wc -l) 73 | if [ $COUNT -gt 1 ]; then EXAMPLE_HASH=$(echo ${EXAMPLE_LIST} | md5sum | cut -f1 -d" "); else EXAMPLE_HASH=$(echo ${EXAMPLE_LIST:1} | sed 's/.$//'); fi 74 | 75 | echo "Examples: ${EXAMPLE_LIST:1}" 76 | echo "Example Hash: ${EXAMPLE_HASH}" 77 | 78 | echo "example-list=${EXAMPLE_LIST:1}" >> $GITHUB_OUTPUT 79 | echo "example-hash=${EXAMPLE_HASH}" >> $GITHUB_OUTPUT 80 | 81 | - name: Create Pending Status Check 82 | env: 83 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | run: | 85 | gh api \ 86 | --method POST \ 87 | -H "Accept: application/vnd.github+json" \ 88 | /repos/${{ github.repository }}/statuses/${{ steps.checkout-pr.outputs.commit-sha }} \ 89 | -f state='pending' \ 90 | -f target_url='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' \ 91 | -f description='Running...' \ 92 | -f context="Uptest-${{ steps.get-example-list-name.outputs.example-hash }}" 93 | 94 | uptest: 95 | needs: 96 | - check-permissions 97 | - get-example-list 98 | if: ${{ (needs.check-permissions.outputs.permission == 'admin' || needs.check-permissions.outputs.permission == 'write') && github.event.issue.pull_request != null && contains(github.event.comment.body, '/test-examples')}} 99 | runs-on: ubuntu-latest 100 | steps: 101 | - name: Cleanup Disk 102 | uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 103 | with: 104 | large-packages: false 105 | swap-storage: false 106 | 107 | - name: Setup QEMU 108 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 109 | with: 110 | platforms: all 111 | 112 | - name: Checkout 113 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 114 | with: 115 | submodules: true 116 | 117 | - name: Setup Go 118 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 119 | with: 120 | go-version: ${{ env.GO_VERSION }} 121 | 122 | - name: Checkout PR 123 | id: checkout-pr 124 | env: 125 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | run: | 127 | gh pr checkout ${{ github.event.issue.number }} 128 | git submodule update --init --recursive 129 | OUTPUT=$(git log -1 --format='%H') 130 | echo "commit-sha=$OUTPUT" >> $GITHUB_OUTPUT 131 | 132 | - name: Vendor Dependencies 133 | run: make vendor vendor.check 134 | 135 | - name: Run Uptest 136 | id: run-uptest 137 | env: 138 | UPTEST_CLOUD_CREDENTIALS: ${{ secrets.UPTEST_CLOUD_CREDENTIALS }} 139 | UPTEST_EXAMPLE_LIST: ${{ needs.get-example-list.outputs.example_list }} 140 | UPTEST_TEST_DIR: ./_output/controlplane-dump 141 | UPTEST_DATASOURCE_PATH: .work/uptest-datasource.yaml 142 | UPTEST_UPDATE_PARAMETER: "" 143 | run: | 144 | mkdir -p .work && echo "${{ secrets.UPTEST_DATASOURCE }}" > .work/uptest-datasource.yaml 145 | make e2e 146 | 147 | - name: Create Successful Status Check 148 | env: 149 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 150 | EXAMPLE_HASH: ${{ needs.get-example-list.outputs.example_hash }} 151 | run: | 152 | gh api \ 153 | --method POST \ 154 | -H "Accept: application/vnd.github+json" \ 155 | /repos/${{ github.repository }}/statuses/${{ steps.checkout-pr.outputs.commit-sha }} \ 156 | -f state='success' \ 157 | -f target_url='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' \ 158 | -f description='Passed' \ 159 | -f context="Uptest-${EXAMPLE_HASH}" 160 | 161 | - name: Collect Cluster Dump 162 | if: always() 163 | run: | 164 | make controlplane.dump 165 | 166 | - name: Upload Cluster Dump 167 | if: always() 168 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 169 | with: 170 | name: controlplane-dump 171 | path: ./_output/controlplane-dump 172 | 173 | - name: Cleanup 174 | if: always() 175 | run: | 176 | eval $(make --no-print-directory build.vars) 177 | ${KUBECTL} delete managed --all || true 178 | 179 | - name: Create Unsuccessful Status Check 180 | if: failure() 181 | env: 182 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 183 | EXAMPLE_HASH: ${{ needs.get-example-list.outputs.example_hash }} 184 | run: | 185 | gh api \ 186 | --method POST \ 187 | -H "Accept: application/vnd.github+json" \ 188 | /repos/${{ github.repository }}/statuses/${{ steps.checkout-pr.outputs.commit-sha }} \ 189 | -f state='failure' \ 190 | -f target_url='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' \ 191 | -f description='Failed' \ 192 | -f context="Uptest-${EXAMPLE_HASH}" 193 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /.work 3 | /_output 4 | cover.out 5 | /vendor 6 | /.vendor-new 7 | .registry 8 | 9 | # ignore GoLand files (debug config etc...) 10 | /.idea 11 | 12 | # ignore vscode files (debug config etc...) 13 | /.vscode 14 | 15 | # ignore asdf local versions 16 | /.tool-versions 17 | 18 | # local tf dir 19 | /tf 20 | 21 | # common credentials 22 | kubeconfig 23 | credentials.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "build"] 2 | path = build 3 | url = https://github.com/crossplane/build 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 The Crossplane Authors 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | version: "2" 6 | output: 7 | formats: 8 | text: 9 | path: stdout 10 | linters: 11 | enable: 12 | - asasalint 13 | - asciicheck 14 | - bidichk 15 | - bodyclose 16 | - contextcheck 17 | - durationcheck 18 | - errchkjson 19 | - errorlint 20 | - exhaustive 21 | - gocheckcompilerdirectives 22 | - gochecksumtype 23 | - goconst 24 | - gocritic 25 | - gocyclo 26 | - gosec 27 | - gosmopolitan 28 | - loggercheck 29 | - makezero 30 | - misspell 31 | - musttag 32 | - nakedret 33 | - nilerr 34 | - nilnesserr 35 | - noctx 36 | - prealloc 37 | - protogetter 38 | - reassign 39 | - recvcheck 40 | - revive 41 | - rowserrcheck 42 | - spancheck 43 | - sqlclosecheck 44 | - testifylint 45 | - unconvert 46 | - unparam 47 | - zerologlint 48 | settings: 49 | dupl: 50 | threshold: 100 51 | errcheck: 52 | check-type-assertions: false 53 | check-blank: false 54 | exclude-functions: 55 | - io/ioutil.ReadFile 56 | - io/ioutil.ReadDir 57 | - io/ioutil.ReadAll 58 | goconst: 59 | min-len: 3 60 | min-occurrences: 5 61 | gocritic: 62 | enabled-tags: 63 | - performance 64 | settings: 65 | captLocal: 66 | paramsOnly: true 67 | rangeValCopy: 68 | sizeThreshold: 32 69 | gocyclo: 70 | min-complexity: 15 71 | lll: 72 | tab-width: 1 73 | nakedret: 74 | max-func-lines: 30 75 | prealloc: 76 | simple: true 77 | range-loops: true 78 | for-loops: false 79 | revive: 80 | confidence: 0.8 81 | unparam: 82 | check-exported: false 83 | exclusions: 84 | generated: lax 85 | rules: 86 | - linters: 87 | - dupl 88 | - errcheck 89 | - gocyclo 90 | - gosec 91 | - scopelint 92 | - unparam 93 | path: _test(ing)?\.go 94 | - linters: 95 | - gocritic 96 | path: _test\.go 97 | text: (unnamedResult|exitAfterDefer) 98 | - linters: 99 | - gocritic 100 | text: '(hugeParam|rangeValCopy):' 101 | - linters: 102 | - staticcheck 103 | text: 'SA3000:' 104 | - linters: 105 | - gosec 106 | text: 'G101:' 107 | - linters: 108 | - gosec 109 | text: 'G104:' 110 | - linters: 111 | - staticcheck 112 | text: 'QF1008:' 113 | - linters: 114 | - staticcheck 115 | text: 'QF1001:' 116 | paths: 117 | - zz_\..+\.go$ 118 | - third_party$ 119 | - builtin$ 120 | - examples$ 121 | issues: 122 | max-issues-per-linter: 0 123 | max-same-issues: 0 124 | new: false 125 | formatters: 126 | enable: 127 | - gofmt 128 | - goimports 129 | settings: 130 | gofmt: 131 | simplify: true 132 | goimports: 133 | local-prefixes: 134 | - github.com/upbound/provider-terraform 135 | exclusions: 136 | generated: lax 137 | paths: 138 | - zz_\..+\.go$ 139 | - third_party$ 140 | - builtin$ 141 | - examples$ 142 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file controls automatic PR reviewer assignment. See the following docs: 2 | # 3 | # * https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 4 | # * https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-settings-for-your-team 5 | # 6 | # The goal of this file is for most PRs to automatically and fairly have one 7 | # maintainer set as PR reviewers. All maintainers have permission to approve 8 | # and merge PRs. All PRs must be approved by at least one maintainer before being merged. 9 | # 10 | # Where possible, prefer explicitly specifying a maintainer who is a subject 11 | # matter expert for a particular part of the codebase rather than using the 12 | # @upbound/team-extensions group. 13 | # 14 | # See also OWNERS.md for governance details 15 | 16 | # Fallback owners 17 | * @bobh66 @ytsarev @negz @sergenyalcin @turkenf @ulucinar @erhancagirici 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to upbound/provider-terraform 2 | 3 | Welcome, and thank you for considering contributing to `provider-terraform`. 4 | We encourage you to help out by raising issues, improving documentation, 5 | fixing bugs, or adding new features. 6 | 7 | If you're interested in contributing please start by reading this document. 8 | If you have any questions at all, or don't know where to start, please reach 9 | out to us in the [`#providers`](https://crossplane.slack.com/archives/C01TRKD4623) 10 | channel in [Crossplane Community Slack]. 11 | 12 | ## Contributing Code 13 | 14 | To contribute bug fixes or features to `provider-terraform`: 15 | 16 | 1. Communicate your intent. 17 | 1. Make your changes. 18 | 1. Test your changes. 19 | 1. Update documentation and examples where appropriate. 20 | 1. Open a Pull Request (PR). 21 | 22 | Communicating your intent lets the [maintainers] know that you intend 23 | to contribute, and how. This sets you up for success - you can avoid duplicating 24 | an effort that may already be underway, adding a feature that may be rejected, 25 | or heading down a path that you would be steered away from at review time. The 26 | best way to communicate your intent is via a detailed GitHub issue. Take a look 27 | first to see if there's already an issue relating to the thing you'd like to 28 | contribute. If there isn't, please raise a new one! Let us know what you'd like 29 | to work on, and why. The `provider-terraform` maintainers can't always triage new issues 30 | immediately, but we encourage you to bring them to our attention via [Slack]. 31 | 32 | Be sure to practice [good git commit hygiene] as you make your changes. All but 33 | the smallest changes should be broken up into a few commits that tell a story. 34 | Use your git commits to provide context for the folks who will review PR, and 35 | the folks who will be spelunking the codebase in the months and years to come. 36 | Ensure each of your commits is signed-off in compliance with the [Developer 37 | Certificate of Origin] by using `git commit -s` and follow the CLA instructions. 38 | The `provider-terraform` project highly values readable, idiomatic Go code. Familiarise 39 | yourself with the [Coding Style] and try to preempt any comments your reviewers 40 | would otherwise leave. Run `make reviewable` to lint your change. 41 | 42 | All `provider-terraform` code must be covered by tests. `provider-terraform` does not use Ginkgo tests and 43 | will request changes to any PR that uses Ginkgo or any third party testing 44 | library, per the common Go [test review comments]. `provider-terraform` encourages the use 45 | of table driven unit tests. 46 | 47 | Note that when opening a PR your reviewer will expect you to detail how you've 48 | tested your work. For all but the smallest changes some manual testing is 49 | encouraged in addition to unit tests. 50 | 51 | All `provider-terraform` documentation is under revision control; see the [docs] directory 52 | of this repository. Any change that introduces new behaviour or changes existing 53 | behaviour must include updates to any relevant documentation. Please keep 54 | documentation changes in distinct commits. 55 | 56 | Once your change is written, tested, and documented the final step is to have it 57 | reviewed! You'll be presented with a template and a small checklist when you 58 | open a PR. Please read the template and fill out the checklist. Please make all 59 | requested changes in subsequent commits. This allows your reviewers to see what 60 | has changed as you address their comments. Be mindful of your commit history as 61 | you do this - avoid commit messages like "Address review feedback" if possible. 62 | If doing so is difficult a good alternative is to rewrite your commit history to 63 | clean them up after your PR is approved but before it is merged. 64 | 65 | In summary, please: 66 | 67 | * Discuss your change in a GitHub issue before you start. 68 | * Use your Git commit messages to communicate your intent to your reviewers. 69 | * Sign-off on all Git commits by running `git commit -s` 70 | * Add or update tests for all changes. 71 | * Preempt [Coding Style] review comments. 72 | * Update all relevant documentation. 73 | * Don't force push to address review feedback. Your commits should tell a story. 74 | * If necessary, tidy up your git commit history once your PR is approved. 75 | 76 | Thank you for reading through our contributing guide! We appreciate you taking 77 | the time to ensure your contributions are high quality and easy for our 78 | community to review and accept. Please don't hesitate to [reach out to 79 | us][Slack] if you have any questions about contributing! 80 | 81 | ## Establishing a Development Environment 82 | 83 | The `provider-terraform` project uses the Upbound [build submodule]; a library of 84 | common Makefiles. Establishing a development environment typically requires: 85 | 86 | 1. Forking and cloning the repository you wish to work on. 87 | 1. Installing development dependencies. 88 | 1. Running `make` to establish the build submodule. 89 | 90 | Run `make help` for information on the available Make targets. Useful targets 91 | include: 92 | 93 | * `make` - Synchronize the build submodule and build provider-terraform. 94 | * `make reviewable` - Run code generation, linters, and unit tests. 95 | * `make e2e` - Run end-to-end tests. 96 | 97 | Once you've built provider-terraform you can deploy it to a Kubernetes cluster of your 98 | choice. [`kind`] (Kubernetes in Docker) is a good choice for development. 99 | 100 | When iterating rapidly on a change it can be faster to run provider-terraform as a local 101 | process. For example: 102 | 103 | ```bash 104 | # Run provider-terraform locally; it should connect to your kind cluster if said cluster 105 | # is your active kubectl context. You can also go run cmd/provider-terraform/main.go. 106 | make run 107 | ``` 108 | 109 | ## Code Review Process 110 | 111 | All Pull Requests (PR), whether written by a maintainer or a 112 | community member, must go through code review. This ensures that the code is 113 | correct, maintainable, and secure. 114 | 115 | We encourage anyone in the community to conduct a code review on a PR, 116 | however, we require the following approvals before merging a PR: 117 | - At least one approval from [Maintainers] 118 | 119 | The PR author should notify one of the [Maintainers], when the PR is ready for 120 | review. 121 | 122 | [Crossplane Community Slack]: https://slack.crossplane.io/ 123 | [Slack]: https://crossplane.slack.com/archives/C01TRKD4623 124 | [maintainers]: https://github.com/upbound/provider-terraform/blob/main/OWNERS.md 125 | [code of conduct]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 126 | [good git commit hygiene]: https://www.futurelearn.com/info/blog/telling-stories-with-your-git-history 127 | [Developer Certificate of Origin]: https://github.com/apps/dco 128 | [test review comments]: https://github.com/golang/go/wiki/TestComments 129 | [docs]: docs/ 130 | [build submodule]: https://github.com/upbound/build/ 131 | [Coding Style]: https://github.com/crossplane/crossplane/blob/master/CONTRIBUTING.md#coding-style 132 | 133 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This project is a larger work that combines with software written by third 2 | parties, licensed under their own terms. 3 | 4 | Notably, this larger work combines with the following Terraform components, 5 | which are licensed under the Mozilla Public License 2.0 (see 6 | or the individual projects listed 7 | below). 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /OWNERS.md: -------------------------------------------------------------------------------- 1 | # OWNERS 2 | 3 | This page lists all maintainers for **this** repository. Each repository in the [Upbound 4 | organization](https://github.com/upbound/) will list their repository maintainers in their own 5 | `OWNERS.md` file. 6 | 7 | ## Maintainers 8 | 9 | * Bob Haddleton ([bobh66](https://github.com/bobh66)) 10 | * Yury Tsarev ([ytsarev](https://github.com/ytsarev)) 11 | * Nic Cope ([negz](https://github.com/negz)) 12 | * Sergen Yalcin ([sergenyalcin](https://github.com/sergenyalcin)) 13 | * Fatih Turken ([turkenf](https://github.com/turkenf)) 14 | * Alper Ulucinar ([ulucinar](https://github.com/ulucinar)) 15 | * Erhan Cagirici ([erhancagirici](https://github.com/erhancagirici)) 16 | 17 | See [CODEOWNERS](./CODEOWNERS) for automatic PR assignment. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Provider 2 | 3 | > [!IMPORTANT] 4 | > This provider is frozen at Terraform 1.5.7 and will not adopt any Terraform versions released under the BSL license. For newer capabilities, consider [provider-opentofu](https://github.com/upbound/provider-opentofu) instead. 5 | 6 |
7 | 8 | ![CI](https://github.com/upbound/provider-terraform/workflows/CI/badge.svg) [![GitHub release](https://img.shields.io/github/release/upbound/provider-terraform/all.svg?style=flat-square)](https://github.com/upbound/provider-terraform/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/upbound/provider-terraform)](https://goreportcard.com/report/github.com/upbound/provider-terraform) [![Slack](https://slack.crossplane.io/badge.svg)](https://crossplane.slack.com/archives/C01TRKD4623) [![Twitter Follow](https://img.shields.io/twitter/follow/upbound_io.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=upbound_io&user_id=788180534543339520) 9 | 10 |
11 | 12 | Provider Terraform is a [Crossplane](https://crossplane.io/) provider that 13 | can run Terraform code and enables defining new Crossplane Composite Resources (XRs) 14 | that are composed of a mix of 'native' Crossplane managed resources and your 15 | existing Terraform modules. 16 | 17 | The Terraform provider adds support for a `Workspace` managed resource that 18 | represents a Terraform workspace. The configuration of each workspace may be 19 | either fetched from a remote source (e.g. git), or simply specified inline. 20 | 21 | ```yaml 22 | apiVersion: tf.upbound.io/v1beta1 23 | kind: Workspace 24 | metadata: 25 | name: example-inline 26 | annotations: 27 | # The terraform workspace will be named 'coolbucket'. If you omitted this 28 | # annotation it would be derived from metadata.name - i.e. 'example-inline'. 29 | crossplane.io/external-name: coolbucket 30 | spec: 31 | forProvider: 32 | # For simple cases you can use an inline source to specify the content of 33 | # main.tf as opaque, inline HCL. 34 | source: Inline 35 | module: | 36 | // All outputs are written to the connection secret. Non-sensitive outputs 37 | // are stored in the status.atProvider.outputs object. 38 | output "url" { 39 | value = google_storage_bucket.example.self_link 40 | } 41 | 42 | resource "random_id" "example" { 43 | byte_length = 4 44 | } 45 | 46 | // The google provider and remote state are configured by the provider 47 | // config - see examples/providerconfig.yaml. 48 | resource "google_storage_bucket" "example" { 49 | name = "crossplane-example-${terraform.workspace}-${random_id.example.hex}" 50 | } 51 | writeConnectionSecretToRef: 52 | namespace: default 53 | name: terraform-workspace-example-inline 54 | ``` 55 | 56 | ```yaml 57 | apiVersion: tf.upbound.io/v1beta1 58 | kind: Workspace 59 | metadata: 60 | name: example-remote 61 | annotations: 62 | crossplane.io/external-name: myworkspace 63 | spec: 64 | forProvider: 65 | # Use any module source supported by terraform init -from-module. 66 | source: Remote 67 | module: https://github.com/crossplane/tf 68 | # Environment variables can be passed through 69 | env: 70 | - name: TF_VAR_varFromValue 71 | value: 'value' 72 | - name: ENV_FROM_CONFIGMAP 73 | configMapKeyRef: 74 | namespace: my-namespace 75 | name: my-config-map 76 | key: target-key 77 | - name: ENV_FROM_SECRET 78 | secretKeyRef: 79 | namespace: my-namespace 80 | name: my-secret 81 | key: target-key 82 | # Variables can be specified inline as a list of key-value pairs or as an json object, or loaded from a ConfigMap or Secret. 83 | vars: 84 | - key: region 85 | value: us-west-1 86 | varmap: 87 | account: 88 | region: us-west-1 89 | owners: 90 | - example-owner-1 91 | - example-owner-2 92 | varFiles: 93 | - source: SecretKey 94 | secretKeyRef: 95 | namespace: default 96 | name: terraform 97 | key: example.tfvar.json 98 | # All Terraform outputs are written to the connection secret. 99 | writeConnectionSecretToRef: 100 | namespace: default 101 | name: terraform-workspace-example-inline 102 | ``` 103 | 104 | ## Getting Started 105 | 106 | Follow the quick start guide [here](https://marketplace.upbound.io/providers/upbound/provider-terraform/latest/docs/quickstart). 107 | 108 | You can find a detailed API reference for all the managed resources with examples in the [Upbound Marketplace](https://marketplace.upbound.io/providers/upbound/provider-terraform/latest/managed-resources). 109 | 110 | ## Further Configuration 111 | 112 | You can find more information about configuring the provider further [here](https://marketplace.upbound.io/providers/upbound/provider-terraform/latest/docs/configuration). 113 | 114 | ### Polling Interval 115 | The default polling interval has been updated to 10 minutes from 1 minute. 116 | This affects how often the provider will run `terraform plan` on existing 117 | `Workspaces` to determine if there are any resources out of sync and whether 118 | `terraform apply` needs to be re-executed to recover the desired state. 119 | A 1 minute polling interval is often too short when the time required for 120 | running `terrform init`, `terraform plan` and `terraform apply` is taken 121 | into account. Workspaces with large numbers of resources can take longer 122 | than 1 minute to run `terraform plan`. Changes to the `Workspace` object 123 | `spec` will still be reconciled immediately. The poll interval is 124 | configurable using `ControllerConfig`. 125 | 126 | ## Known limitations: 127 | 128 | * You must either use remote state or ensure the provider container's `/tf` 129 | directory is not lost. `provider-terraform` __does not persist state__; 130 | consider using the [Kubernetes](https://www.terraform.io/docs/language/settings/backends/kubernetes.html) remote state backend. 131 | * If the module takes longer than the value of `--timeout` (default is 20m) to apply the 132 | underlying `terraform` process will be killed. You will potentially lose state 133 | and leak resources. The workspace lock will also likely be left in place and need to be manually removed 134 | before the Workspace can be reconciled again. 135 | * The provider won't emit an event until _after_ it has successfully applied the 136 | Terraform module, which can take a long time. 137 | * Setting `--max-reconcile-rate` to a value greater than 1 will potentially cause the provider 138 | to use up to the same number of CPUs. Add a resources section to the ControllerConfig to restrict 139 | CPU usage as needed. 140 | 141 | ## Report a Bug 142 | 143 | For filing bugs, suggesting improvements, or requesting new features, please 144 | open an [issue](https://github.com/upbound/provider-terraform/issues). 145 | 146 | ## Contact 147 | 148 | Please open a Github issue for all requests. If you need to reach out to Upbound, 149 | you can do so via the following channels: 150 | * Slack: [#upbound](https://crossplane.slack.com/archives/C01TRKD4623) channel in [Crossplane Slack](https://slack.crossplane.io) 151 | * Twitter: [@upbound_io](https://twitter.com/upbound_io) 152 | * Email: [support@upbound.io](mailto:support@upbound.io) 153 | 154 | ## Licensing 155 | 156 | Provider Terraform is under [the Apache 2.0 license](LICENSE) with [notice](NOTICE). 157 | -------------------------------------------------------------------------------- /apis/cluster/terraform.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package cluster contains Kubernetes API for the terraform provider. 18 | package cluster 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime" 22 | 23 | "github.com/upbound/provider-terraform/apis/cluster/v1beta1" 24 | ) 25 | 26 | func init() { 27 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 28 | AddToSchemes = append(AddToSchemes, 29 | v1beta1.SchemeBuilder.AddToScheme, 30 | ) 31 | } 32 | 33 | // AddToSchemes may be used to add all resources defined in the project to a Scheme 34 | var AddToSchemes runtime.SchemeBuilder 35 | 36 | // AddToScheme adds all Resources to the Scheme 37 | func AddToScheme(s *runtime.Scheme) error { 38 | return AddToSchemes.AddToScheme(s) 39 | } 40 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1beta1 contains the core resources of the Terraform provider. 18 | // +kubebuilder:object:generate=true 19 | // +groupName=tf.upbound.io 20 | // +versionName=v1beta1 21 | package v1beta1 22 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "reflect" 21 | 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | "sigs.k8s.io/controller-runtime/pkg/scheme" 24 | ) 25 | 26 | // Package type metadata. 27 | const ( 28 | Group = "tf.upbound.io" 29 | Version = "v1beta1" 30 | ) 31 | 32 | var ( 33 | // SchemeGroupVersion is group version used to register these objects 34 | SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} 35 | 36 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 37 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 38 | ) 39 | 40 | // ProviderConfig type metadata. 41 | var ( 42 | ProviderConfigKind = reflect.TypeOf(ProviderConfig{}).Name() 43 | ProviderConfigGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigKind}.String() 44 | ProviderConfigKindAPIVersion = ProviderConfigKind + "." + SchemeGroupVersion.String() 45 | ProviderConfigGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigKind) 46 | ) 47 | 48 | // ProviderConfigUsage type metadata. 49 | var ( 50 | ProviderConfigUsageKind = reflect.TypeOf(ProviderConfigUsage{}).Name() 51 | ProviderConfigUsageGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigUsageKind}.String() 52 | ProviderConfigUsageKindAPIVersion = ProviderConfigUsageKind + "." + SchemeGroupVersion.String() 53 | ProviderConfigUsageGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigUsageKind) 54 | 55 | ProviderConfigUsageListKind = reflect.TypeOf(ProviderConfigUsageList{}).Name() 56 | ProviderConfigUsageListGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigUsageListKind}.String() 57 | ProviderConfigUsageListKindAPIVersion = ProviderConfigUsageListKind + "." + SchemeGroupVersion.String() 58 | ProviderConfigUsageListGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigUsageListKind) 59 | ) 60 | 61 | // Workspace type metadata. 62 | var ( 63 | WorkspaceKind = reflect.TypeOf(Workspace{}).Name() 64 | WorkspaceGroupKind = schema.GroupKind{Group: Group, Kind: WorkspaceKind}.String() 65 | WorkspaceKindAPIVersion = WorkspaceKind + "." + SchemeGroupVersion.String() 66 | WorkspaceGroupVersionKind = SchemeGroupVersion.WithKind(WorkspaceKind) 67 | ) 68 | 69 | func init() { 70 | SchemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{}) 71 | SchemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{}) 72 | SchemeBuilder.Register(&Workspace{}, &WorkspaceList{}) 73 | } 74 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 23 | ) 24 | 25 | // A ProviderConfigSpec defines the desired state of a ProviderConfig. 26 | type ProviderConfigSpec struct { 27 | // Credentials required to authenticate to this provider. 28 | // +optional 29 | Credentials []ProviderCredentials `json:"credentials"` 30 | 31 | // Configuration that should be injected into all workspaces that use 32 | // this provider config, expressed as inline HCL. This can be used to 33 | // automatically inject Terraform provider configuration blocks. 34 | // +optional 35 | Configuration *string `json:"configuration,omitempty"` 36 | 37 | // Terraform backend file configuration content, 38 | // it has the contents of the backend block as top-level attributes, 39 | // without the need to wrap it in another terraform or backend block. 40 | // More details at https://developer.hashicorp.com/terraform/language/settings/backends/configuration#file. 41 | // +optional 42 | BackendFile *string `json:"backendFile,omitempty"` 43 | 44 | // PluginCache enables terraform provider plugin caching mechanism 45 | // https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache 46 | // +optional 47 | // +kubebuilder:default=true 48 | PluginCache *bool `json:"pluginCache,omitempty"` 49 | } 50 | 51 | // ProviderCredentials required to authenticate. 52 | type ProviderCredentials struct { 53 | // Filename (relative to main.tf) to which these provider credentials 54 | // should be written. 55 | Filename string `json:"filename"` 56 | 57 | // Source of the provider credentials. 58 | // +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem 59 | Source xpv1.CredentialsSource `json:"source"` 60 | 61 | xpv1.CommonCredentialSelectors `json:",inline"` 62 | } 63 | 64 | // A ProviderConfigStatus reflects the observed state of a ProviderConfig. 65 | type ProviderConfigStatus struct { 66 | xpv1.ProviderConfigStatus `json:",inline"` 67 | } 68 | 69 | // +kubebuilder:object:root=true 70 | 71 | // A ProviderConfig configures a Terraform provider. 72 | // +kubebuilder:subresource:status 73 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 74 | // +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentials.secretRef.name",priority=1 75 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,terraform} 76 | type ProviderConfig struct { 77 | metav1.TypeMeta `json:",inline"` 78 | metav1.ObjectMeta `json:"metadata,omitempty"` 79 | 80 | Spec ProviderConfigSpec `json:"spec"` 81 | Status ProviderConfigStatus `json:"status,omitempty"` 82 | } 83 | 84 | // +kubebuilder:object:root=true 85 | 86 | // ProviderConfigList contains a list of ProviderConfig. 87 | type ProviderConfigList struct { 88 | metav1.TypeMeta `json:",inline"` 89 | metav1.ListMeta `json:"metadata,omitempty"` 90 | Items []ProviderConfig `json:"items"` 91 | } 92 | 93 | // +kubebuilder:object:root=true 94 | 95 | // A ProviderConfigUsage indicates that a resource is using a ProviderConfig. 96 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 97 | // +kubebuilder:printcolumn:name="CONFIG-NAME",type="string",JSONPath=".providerConfigRef.name" 98 | // +kubebuilder:printcolumn:name="RESOURCE-KIND",type="string",JSONPath=".resourceRef.kind" 99 | // +kubebuilder:printcolumn:name="RESOURCE-NAME",type="string",JSONPath=".resourceRef.name" 100 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,terraform} 101 | type ProviderConfigUsage struct { 102 | metav1.TypeMeta `json:",inline"` 103 | metav1.ObjectMeta `json:"metadata,omitempty"` 104 | 105 | xpv1.ProviderConfigUsage `json:",inline"` 106 | } 107 | 108 | // +kubebuilder:object:root=true 109 | 110 | // ProviderConfigUsageList contains a list of ProviderConfigUsage 111 | type ProviderConfigUsageList struct { 112 | metav1.TypeMeta `json:",inline"` 113 | metav1.ListMeta `json:"metadata,omitempty"` 114 | Items []ProviderConfigUsage `json:"items"` 115 | } 116 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/workspace_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | extensionsV1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | runtime "k8s.io/apimachinery/pkg/runtime" 24 | ) 25 | 26 | // A Var represents a Terraform configuration variable. 27 | type Var struct { 28 | Key string `json:"key"` 29 | Value string `json:"value"` 30 | } 31 | 32 | // A VarFileSource specifies the source of a Terraform vars file. 33 | // +kubebuilder:validation:Enum=ConfigMapKey;SecretKey 34 | type VarFileSource string 35 | 36 | // Vars file sources. 37 | const ( 38 | VarFileSourceConfigMapKey VarFileSource = "ConfigMapKey" 39 | VarFileSourceSecretKey VarFileSource = "SecretKey" 40 | ) 41 | 42 | // A FileFormat specifies the format of a Terraform file. 43 | // +kubebuilder:validation:Enum=HCL;JSON 44 | type FileFormat string 45 | 46 | // Vars file formats. 47 | var ( 48 | FileFormatHCL FileFormat = "HCL" 49 | FileFormatJSON FileFormat = "JSON" 50 | ) 51 | 52 | // A VarFile is a file containing many Terraform variables. 53 | type VarFile struct { 54 | // Source of this vars file. 55 | Source VarFileSource `json:"source"` 56 | 57 | // Format of this vars file. 58 | // +kubebuilder:default=HCL 59 | // +optional 60 | Format *FileFormat `json:"format,omitempty"` 61 | 62 | // A ConfigMap key containing the vars file. 63 | // +optional 64 | ConfigMapKeyReference *KeyReference `json:"configMapKeyRef,omitempty"` 65 | 66 | // A Secret key containing the vars file. 67 | // +optional 68 | SecretKeyReference *KeyReference `json:"secretKeyRef,omitempty"` 69 | } 70 | 71 | // An EnvVar specifies an environment variable to be set for the workspace. 72 | type EnvVar struct { 73 | Name string `json:"name"` 74 | Value string `json:"value,omitempty"` 75 | 76 | // A ConfigMap key containing the desired env var value. 77 | ConfigMapKeyReference *KeyReference `json:"configMapKeyRef,omitempty"` 78 | 79 | // A Secret key containing the desired env var value. 80 | SecretKeyReference *KeyReference `json:"secretKeyRef,omitempty"` 81 | } 82 | 83 | // A KeyReference references a key within a Secret or a ConfigMap. 84 | type KeyReference struct { 85 | // Namespace of the referenced resource. 86 | Namespace string `json:"namespace"` 87 | 88 | // Name of the referenced resource. 89 | Name string `json:"name"` 90 | 91 | // Key within the referenced resource. 92 | Key string `json:"key"` 93 | } 94 | 95 | // A ModuleSource represents the source of a Terraform module. 96 | // +kubebuilder:validation:Enum=Remote;Inline;Flux 97 | type ModuleSource string 98 | 99 | // Module sources. 100 | const ( 101 | ModuleSourceRemote ModuleSource = "Remote" 102 | ModuleSourceInline ModuleSource = "Inline" 103 | ModuleSourceFlux ModuleSource = "Flux" 104 | ) 105 | 106 | // WorkspaceParameters are the configurable fields of a Workspace. 107 | type WorkspaceParameters struct { 108 | // The root module of this workspace; i.e. the module containing its main.tf 109 | // file. When the workspace's source is 'Remote' (the default) this can be 110 | // any address supported by terraform init -from-module, for example a git 111 | // repository or an S3 bucket. When the workspace's source is 'Inline' the 112 | // content of a simple main.tf or main.tf.json file may be written inline. 113 | // When the workspace's source is 'Flux', use the FluxSourceKind::namespace/name format. 114 | // Example: 115 | // Module: "GitRepository::my-namespace/my-repo" 116 | Module string `json:"module"` 117 | 118 | // Specifies the format of the inline Terraform content 119 | // if Source is 'Inline' 120 | InlineFormat FileFormat `json:"inlineFormat,omitempty"` 121 | 122 | // Source of the root module of this workspace. 123 | Source ModuleSource `json:"source"` 124 | 125 | // Entrypoint for `terraform init` within the module 126 | // +kubebuilder:default="" 127 | // +optional 128 | Entrypoint string `json:"entrypoint"` 129 | 130 | // Environment variables. 131 | // +optional 132 | Env []EnvVar `json:"env,omitempty"` 133 | 134 | // Configuration variables. 135 | // +optional 136 | Vars []Var `json:"vars,omitempty"` 137 | 138 | // Terraform Variable Map. Should be a valid JSON representation of the input vars 139 | // +optional 140 | VarMap *runtime.RawExtension `json:"varmap,omitempty"` 141 | 142 | // Files of configuration variables. Explicitly declared vars take 143 | // precedence. 144 | // +optional 145 | VarFiles []VarFile `json:"varFiles,omitempty"` 146 | 147 | // Arguments to be included in the terraform init CLI command 148 | InitArgs []string `json:"initArgs,omitempty"` 149 | 150 | // Arguments to be included in the terraform plan CLI command 151 | PlanArgs []string `json:"planArgs,omitempty"` 152 | 153 | // Arguments to be included in the terraform apply CLI command 154 | ApplyArgs []string `json:"applyArgs,omitempty"` 155 | 156 | // Arguments to be included in the terraform destroy CLI command 157 | DestroyArgs []string `json:"destroyArgs,omitempty"` 158 | 159 | // Boolean value to indicate CLI logging of terraform execution is enabled or not 160 | // +optional 161 | EnableTerraformCLILogging bool `json:"enableTerraformCLILogging,omitempty"` 162 | } 163 | 164 | // WorkspaceObservation are the observable fields of a Workspace. 165 | type WorkspaceObservation struct { 166 | Checksum string `json:"checksum,omitempty"` 167 | Outputs map[string]extensionsV1.JSON `json:"outputs,omitempty"` 168 | } 169 | 170 | // A WorkspaceSpec defines the desired state of a Workspace. 171 | type WorkspaceSpec struct { 172 | xpv1.ResourceSpec `json:",inline"` 173 | ForProvider WorkspaceParameters `json:"forProvider"` 174 | } 175 | 176 | // A WorkspaceStatus represents the observed state of a Workspace. 177 | type WorkspaceStatus struct { 178 | xpv1.ResourceStatus `json:",inline"` 179 | AtProvider WorkspaceObservation `json:"atProvider,omitempty"` 180 | } 181 | 182 | // +kubebuilder:object:root=true 183 | 184 | // A Workspace of Terraform Configuration. 185 | // +kubebuilder:subresource:status 186 | // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" 187 | // +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" 188 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 189 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,terraform} 190 | type Workspace struct { 191 | metav1.TypeMeta `json:",inline"` 192 | metav1.ObjectMeta `json:"metadata,omitempty"` 193 | 194 | Spec WorkspaceSpec `json:"spec"` 195 | Status WorkspaceStatus `json:"status,omitempty"` 196 | } 197 | 198 | // +kubebuilder:object:root=true 199 | 200 | // WorkspaceList contains a list of Workspace 201 | type WorkspaceList struct { 202 | metav1.TypeMeta `json:",inline"` 203 | metav1.ListMeta `json:"metadata,omitempty"` 204 | Items []Workspace `json:"items"` 205 | } 206 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/zz_generated.managed.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | 22 | // GetCondition of this Workspace. 23 | func (mg *Workspace) GetCondition(ct xpv1.ConditionType) xpv1.Condition { 24 | return mg.Status.GetCondition(ct) 25 | } 26 | 27 | // GetDeletionPolicy of this Workspace. 28 | func (mg *Workspace) GetDeletionPolicy() xpv1.DeletionPolicy { 29 | return mg.Spec.DeletionPolicy 30 | } 31 | 32 | // GetManagementPolicies of this Workspace. 33 | func (mg *Workspace) GetManagementPolicies() xpv1.ManagementPolicies { 34 | return mg.Spec.ManagementPolicies 35 | } 36 | 37 | // GetProviderConfigReference of this Workspace. 38 | func (mg *Workspace) GetProviderConfigReference() *xpv1.Reference { 39 | return mg.Spec.ProviderConfigReference 40 | } 41 | 42 | // GetWriteConnectionSecretToReference of this Workspace. 43 | func (mg *Workspace) GetWriteConnectionSecretToReference() *xpv1.SecretReference { 44 | return mg.Spec.WriteConnectionSecretToReference 45 | } 46 | 47 | // SetConditions of this Workspace. 48 | func (mg *Workspace) SetConditions(c ...xpv1.Condition) { 49 | mg.Status.SetConditions(c...) 50 | } 51 | 52 | // SetDeletionPolicy of this Workspace. 53 | func (mg *Workspace) SetDeletionPolicy(r xpv1.DeletionPolicy) { 54 | mg.Spec.DeletionPolicy = r 55 | } 56 | 57 | // SetManagementPolicies of this Workspace. 58 | func (mg *Workspace) SetManagementPolicies(r xpv1.ManagementPolicies) { 59 | mg.Spec.ManagementPolicies = r 60 | } 61 | 62 | // SetProviderConfigReference of this Workspace. 63 | func (mg *Workspace) SetProviderConfigReference(r *xpv1.Reference) { 64 | mg.Spec.ProviderConfigReference = r 65 | } 66 | 67 | // SetWriteConnectionSecretToReference of this Workspace. 68 | func (mg *Workspace) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { 69 | mg.Spec.WriteConnectionSecretToReference = r 70 | } 71 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/zz_generated.managedlist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 21 | 22 | // GetItems of this WorkspaceList. 23 | func (l *WorkspaceList) GetItems() []resource.Managed { 24 | items := make([]resource.Managed, len(l.Items)) 25 | for i := range l.Items { 26 | items[i] = &l.Items[i] 27 | } 28 | return items 29 | } 30 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/zz_generated.pc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | 22 | // GetCondition of this ProviderConfig. 23 | func (p *ProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition { 24 | return p.Status.GetCondition(ct) 25 | } 26 | 27 | // GetUsers of this ProviderConfig. 28 | func (p *ProviderConfig) GetUsers() int64 { 29 | return p.Status.Users 30 | } 31 | 32 | // SetConditions of this ProviderConfig. 33 | func (p *ProviderConfig) SetConditions(c ...xpv1.Condition) { 34 | p.Status.SetConditions(c...) 35 | } 36 | 37 | // SetUsers of this ProviderConfig. 38 | func (p *ProviderConfig) SetUsers(i int64) { 39 | p.Status.Users = i 40 | } 41 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/zz_generated.pcu.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | 22 | // GetProviderConfigReference of this ProviderConfigUsage. 23 | func (p *ProviderConfigUsage) GetProviderConfigReference() xpv1.Reference { 24 | return p.ProviderConfigReference 25 | } 26 | 27 | // GetResourceReference of this ProviderConfigUsage. 28 | func (p *ProviderConfigUsage) GetResourceReference() xpv1.TypedReference { 29 | return p.ResourceReference 30 | } 31 | 32 | // SetProviderConfigReference of this ProviderConfigUsage. 33 | func (p *ProviderConfigUsage) SetProviderConfigReference(r xpv1.Reference) { 34 | p.ProviderConfigReference = r 35 | } 36 | 37 | // SetResourceReference of this ProviderConfigUsage. 38 | func (p *ProviderConfigUsage) SetResourceReference(r xpv1.TypedReference) { 39 | p.ResourceReference = r 40 | } 41 | -------------------------------------------------------------------------------- /apis/cluster/v1beta1/zz_generated.pculist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 21 | 22 | // GetItems of this ProviderConfigUsageList. 23 | func (p *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { 24 | items := make([]resource.ProviderConfigUsage, len(p.Items)) 25 | for i := range p.Items { 26 | items[i] = &p.Items[i] 27 | } 28 | return items 29 | } 30 | -------------------------------------------------------------------------------- /apis/namespaced/terraform.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package namespaced contains Kubernetes API for the terraform provider. 18 | package namespaced 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime" 22 | 23 | "github.com/upbound/provider-terraform/apis/namespaced/v1beta1" 24 | ) 25 | 26 | func init() { 27 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 28 | AddToSchemes = append(AddToSchemes, 29 | v1beta1.SchemeBuilder.AddToScheme, 30 | ) 31 | } 32 | 33 | // AddToSchemes may be used to add all resources defined in the project to a Scheme 34 | var AddToSchemes runtime.SchemeBuilder 35 | 36 | // AddToScheme adds all Resources to the Scheme 37 | func AddToScheme(s *runtime.Scheme) error { 38 | return AddToSchemes.AddToScheme(s) 39 | } 40 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1beta1 contains the core resources of the Terraform provider. 18 | // +kubebuilder:object:generate=true 19 | // +groupName=tf.m.upbound.io 20 | // +versionName=v1beta1 21 | package v1beta1 22 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "reflect" 21 | 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | "sigs.k8s.io/controller-runtime/pkg/scheme" 24 | ) 25 | 26 | // Package type metadata. 27 | const ( 28 | Group = "tf.m.upbound.io" 29 | Version = "v1beta1" 30 | ) 31 | 32 | var ( 33 | // SchemeGroupVersion is group version used to register these objects 34 | SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} 35 | 36 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 37 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 38 | ) 39 | 40 | // ProviderConfig type metadata. 41 | var ( 42 | ProviderConfigKind = reflect.TypeOf(ProviderConfig{}).Name() 43 | ProviderConfigGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigKind}.String() 44 | ProviderConfigKindAPIVersion = ProviderConfigKind + "." + SchemeGroupVersion.String() 45 | ProviderConfigGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigKind) 46 | ) 47 | 48 | // ProviderConfigUsage type metadata. 49 | var ( 50 | ProviderConfigUsageKind = reflect.TypeOf(ProviderConfigUsage{}).Name() 51 | ProviderConfigUsageGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigUsageKind}.String() 52 | ProviderConfigUsageKindAPIVersion = ProviderConfigUsageKind + "." + SchemeGroupVersion.String() 53 | ProviderConfigUsageGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigUsageKind) 54 | 55 | ProviderConfigUsageListKind = reflect.TypeOf(ProviderConfigUsageList{}).Name() 56 | ProviderConfigUsageListGroupKind = schema.GroupKind{Group: Group, Kind: ProviderConfigUsageListKind}.String() 57 | ProviderConfigUsageListKindAPIVersion = ProviderConfigUsageListKind + "." + SchemeGroupVersion.String() 58 | ProviderConfigUsageListGroupVersionKind = SchemeGroupVersion.WithKind(ProviderConfigUsageListKind) 59 | 60 | ClusterProviderConfigKind = reflect.TypeOf(ClusterProviderConfig{}).Name() 61 | ClusterProviderConfigGroupKind = schema.GroupKind{Group: Group, Kind: ClusterProviderConfigKind}.String() 62 | ClusterProviderConfigKindAPIVersion = ClusterProviderConfigKind + "." + SchemeGroupVersion.String() 63 | ClusterProviderConfigGroupVersionKind = SchemeGroupVersion.WithKind(ClusterProviderConfigKind) 64 | ) 65 | 66 | // Workspace type metadata. 67 | var ( 68 | WorkspaceKind = reflect.TypeOf(Workspace{}).Name() 69 | WorkspaceGroupKind = schema.GroupKind{Group: Group, Kind: WorkspaceKind}.String() 70 | WorkspaceKindAPIVersion = WorkspaceKind + "." + SchemeGroupVersion.String() 71 | WorkspaceGroupVersionKind = SchemeGroupVersion.WithKind(WorkspaceKind) 72 | ) 73 | 74 | func init() { 75 | SchemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{}) 76 | SchemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{}) 77 | SchemeBuilder.Register(&Workspace{}, &WorkspaceList{}) 78 | SchemeBuilder.Register(&ClusterProviderConfig{}, &ClusterProviderConfigList{}) 79 | } 80 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 23 | xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" 24 | ) 25 | 26 | // A ProviderConfigSpec defines the desired state of a ProviderConfig. 27 | type ProviderConfigSpec struct { 28 | // Credentials required to authenticate to this provider. 29 | // +optional 30 | Credentials []ProviderCredentials `json:"credentials"` 31 | 32 | // Configuration that should be injected into all workspaces that use 33 | // this provider config, expressed as inline HCL. This can be used to 34 | // automatically inject Terraform provider configuration blocks. 35 | // +optional 36 | Configuration *string `json:"configuration,omitempty"` 37 | 38 | // Terraform backend file configuration content, 39 | // it has the contents of the backend block as top-level attributes, 40 | // without the need to wrap it in another terraform or backend block. 41 | // More details at https://developer.hashicorp.com/terraform/language/settings/backends/configuration#file. 42 | // +optional 43 | BackendFile *string `json:"backendFile,omitempty"` 44 | 45 | // PluginCache enables terraform provider plugin caching mechanism 46 | // https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache 47 | // +optional 48 | // +kubebuilder:default=true 49 | PluginCache *bool `json:"pluginCache,omitempty"` 50 | } 51 | 52 | // ProviderCredentials required to authenticate. 53 | type ProviderCredentials struct { 54 | // Filename (relative to main.tf) to which these provider credentials 55 | // should be written. 56 | Filename string `json:"filename"` 57 | 58 | // Source of the provider credentials. 59 | // +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem 60 | Source xpv1.CredentialsSource `json:"source"` 61 | 62 | xpv1.CommonCredentialSelectors `json:",inline"` 63 | } 64 | 65 | // A ProviderConfigStatus reflects the observed state of a ProviderConfig. 66 | type ProviderConfigStatus struct { 67 | xpv1.ProviderConfigStatus `json:",inline"` 68 | } 69 | 70 | // +kubebuilder:object:root=true 71 | 72 | // A ProviderConfig configures a Terraform provider. 73 | // +kubebuilder:subresource:status 74 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 75 | // +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentials.secretRef.name",priority=1 76 | // +kubebuilder:resource:scope=Namespaced,categories={crossplane,provider,terraform} 77 | type ProviderConfig struct { 78 | metav1.TypeMeta `json:",inline"` 79 | metav1.ObjectMeta `json:"metadata,omitempty"` 80 | 81 | Spec ProviderConfigSpec `json:"spec"` 82 | Status ProviderConfigStatus `json:"status,omitempty"` 83 | } 84 | 85 | // +kubebuilder:object:root=true 86 | 87 | // ProviderConfigList contains a list of ProviderConfig. 88 | type ProviderConfigList struct { 89 | metav1.TypeMeta `json:",inline"` 90 | metav1.ListMeta `json:"metadata,omitempty"` 91 | Items []ProviderConfig `json:"items"` 92 | } 93 | 94 | // +kubebuilder:object:root=true 95 | 96 | // A ProviderConfigUsage indicates that a resource is using a ProviderConfig. 97 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 98 | // +kubebuilder:printcolumn:name="CONFIG-NAME",type="string",JSONPath=".providerConfigRef.name" 99 | // +kubebuilder:printcolumn:name="RESOURCE-KIND",type="string",JSONPath=".resourceRef.kind" 100 | // +kubebuilder:printcolumn:name="RESOURCE-NAME",type="string",JSONPath=".resourceRef.name" 101 | // +kubebuilder:resource:scope=Namespaced,categories={crossplane,provider,terraform} 102 | type ProviderConfigUsage struct { 103 | metav1.TypeMeta `json:",inline"` 104 | metav1.ObjectMeta `json:"metadata,omitempty"` 105 | 106 | xpv2.TypedProviderConfigUsage `json:",inline"` 107 | } 108 | 109 | // +kubebuilder:object:root=true 110 | 111 | // ProviderConfigUsageList contains a list of ProviderConfigUsage 112 | type ProviderConfigUsageList struct { 113 | metav1.TypeMeta `json:",inline"` 114 | metav1.ListMeta `json:"metadata,omitempty"` 115 | Items []ProviderConfigUsage `json:"items"` 116 | } 117 | 118 | // +kubebuilder:object:root=true 119 | 120 | // A ClusterProviderConfig configures a Terraform provider. 121 | // +kubebuilder:subresource:status 122 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 123 | // +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentials.secretRef.name",priority=1 124 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,terraform} 125 | type ClusterProviderConfig struct { 126 | metav1.TypeMeta `json:",inline"` 127 | metav1.ObjectMeta `json:"metadata,omitempty"` 128 | 129 | Spec ProviderConfigSpec `json:"spec"` 130 | Status ProviderConfigStatus `json:"status,omitempty"` 131 | } 132 | 133 | // +kubebuilder:object:root=true 134 | 135 | // ClusterProviderConfigList contains a list of ClusterProviderConfig. 136 | type ClusterProviderConfigList struct { 137 | metav1.TypeMeta `json:",inline"` 138 | metav1.ListMeta `json:"metadata,omitempty"` 139 | Items []ClusterProviderConfig `json:"items"` 140 | } 141 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/workspace_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" 22 | extensionsV1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // A Var represents a Terraform configuration variable. 28 | type Var struct { 29 | Key string `json:"key"` 30 | Value string `json:"value"` 31 | } 32 | 33 | // A VarFileSource specifies the source of a Terraform vars file. 34 | // +kubebuilder:validation:Enum=ConfigMapKey;SecretKey 35 | type VarFileSource string 36 | 37 | // Vars file sources. 38 | const ( 39 | VarFileSourceConfigMapKey VarFileSource = "ConfigMapKey" 40 | VarFileSourceSecretKey VarFileSource = "SecretKey" 41 | ) 42 | 43 | // A FileFormat specifies the format of a Terraform file. 44 | // +kubebuilder:validation:Enum=HCL;JSON 45 | type FileFormat string 46 | 47 | // Vars file formats. 48 | var ( 49 | FileFormatHCL FileFormat = "HCL" 50 | FileFormatJSON FileFormat = "JSON" 51 | ) 52 | 53 | // A VarFile is a file containing many Terraform variables. 54 | type VarFile struct { 55 | // Source of this vars file. 56 | Source VarFileSource `json:"source"` 57 | 58 | // Format of this vars file. 59 | // +kubebuilder:default=HCL 60 | // +optional 61 | Format *FileFormat `json:"format,omitempty"` 62 | 63 | // A ConfigMap key containing the vars file. 64 | // +optional 65 | ConfigMapKeyReference *KeyReference `json:"configMapKeyRef,omitempty"` 66 | 67 | // A Secret key containing the vars file. 68 | // +optional 69 | SecretKeyReference *KeyReference `json:"secretKeyRef,omitempty"` 70 | } 71 | 72 | // An EnvVar specifies an environment variable to be set for the workspace. 73 | type EnvVar struct { 74 | Name string `json:"name"` 75 | Value string `json:"value,omitempty"` 76 | 77 | // A ConfigMap key containing the desired env var value. 78 | ConfigMapKeyReference *KeyReference `json:"configMapKeyRef,omitempty"` 79 | 80 | // A Secret key containing the desired env var value. 81 | SecretKeyReference *KeyReference `json:"secretKeyRef,omitempty"` 82 | } 83 | 84 | // A KeyReference references a key within a Secret or a ConfigMap. 85 | type KeyReference struct { 86 | // Name of the referenced resource. 87 | Name string `json:"name"` 88 | 89 | // Key within the referenced resource. 90 | Key string `json:"key"` 91 | } 92 | 93 | // A ModuleSource represents the source of a Terraform module. 94 | // +kubebuilder:validation:Enum=Remote;Inline;Flux 95 | type ModuleSource string 96 | 97 | // Module sources. 98 | const ( 99 | ModuleSourceRemote ModuleSource = "Remote" 100 | ModuleSourceInline ModuleSource = "Inline" 101 | ModuleSourceFlux ModuleSource = "Flux" 102 | ) 103 | 104 | // WorkspaceParameters are the configurable fields of a Workspace. 105 | type WorkspaceParameters struct { 106 | // The root module of this workspace; i.e. the module containing its main.tf 107 | // file. When the workspace's source is 'Remote' (the default) this can be 108 | // any address supported by terraform init -from-module, for example a git 109 | // repository or an S3 bucket. When the workspace's source is 'Inline' the 110 | // content of a simple main.tf or main.tf.json file may be written inline. 111 | // When the workspace's source is 'Flux', use the FluxSourceKind::namespace/name format. 112 | // Example: 113 | // Module: "GitRepository::my-namespace/my-repo" 114 | Module string `json:"module"` 115 | 116 | // Specifies the format of the inline Terraform content 117 | // if Source is 'Inline' 118 | InlineFormat FileFormat `json:"inlineFormat,omitempty"` 119 | 120 | // Source of the root module of this workspace. 121 | Source ModuleSource `json:"source"` 122 | 123 | // Entrypoint for `terraform init` within the module 124 | // +kubebuilder:default="" 125 | // +optional 126 | Entrypoint string `json:"entrypoint"` 127 | 128 | // Environment variables. 129 | // +optional 130 | Env []EnvVar `json:"env,omitempty"` 131 | 132 | // Configuration variables. 133 | // +optional 134 | Vars []Var `json:"vars,omitempty"` 135 | 136 | // Terraform Variable Map. Should be a valid JSON representation of the input vars 137 | // +optional 138 | VarMap *runtime.RawExtension `json:"varmap,omitempty"` 139 | 140 | // Files of configuration variables. Explicitly declared vars take 141 | // precedence. 142 | // +optional 143 | VarFiles []VarFile `json:"varFiles,omitempty"` 144 | 145 | // Arguments to be included in the terraform init CLI command 146 | InitArgs []string `json:"initArgs,omitempty"` 147 | 148 | // Arguments to be included in the terraform plan CLI command 149 | PlanArgs []string `json:"planArgs,omitempty"` 150 | 151 | // Arguments to be included in the terraform apply CLI command 152 | ApplyArgs []string `json:"applyArgs,omitempty"` 153 | 154 | // Arguments to be included in the terraform destroy CLI command 155 | DestroyArgs []string `json:"destroyArgs,omitempty"` 156 | 157 | // Boolean value to indicate CLI logging of terraform execution is enabled or not 158 | // +optional 159 | EnableTerraformCLILogging bool `json:"enableTerraformCLILogging,omitempty"` 160 | } 161 | 162 | // WorkspaceObservation are the observable fields of a Workspace. 163 | type WorkspaceObservation struct { 164 | Checksum string `json:"checksum,omitempty"` 165 | Outputs map[string]extensionsV1.JSON `json:"outputs,omitempty"` 166 | } 167 | 168 | // A WorkspaceSpec defines the desired state of a Workspace. 169 | type WorkspaceSpec struct { 170 | xpv2.ManagedResourceSpec `json:",inline"` 171 | ForProvider WorkspaceParameters `json:"forProvider"` 172 | } 173 | 174 | // A WorkspaceStatus represents the observed state of a Workspace. 175 | type WorkspaceStatus struct { 176 | xpv1.ResourceStatus `json:",inline"` 177 | AtProvider WorkspaceObservation `json:"atProvider,omitempty"` 178 | } 179 | 180 | // +kubebuilder:object:root=true 181 | 182 | // A Workspace of Terraform Configuration. 183 | // +kubebuilder:subresource:status 184 | // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" 185 | // +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" 186 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 187 | // +kubebuilder:resource:scope=Namespaced,categories={crossplane,managed,terraform} 188 | type Workspace struct { 189 | metav1.TypeMeta `json:",inline"` 190 | metav1.ObjectMeta `json:"metadata,omitempty"` 191 | 192 | Spec WorkspaceSpec `json:"spec"` 193 | Status WorkspaceStatus `json:"status,omitempty"` 194 | } 195 | 196 | // +kubebuilder:object:root=true 197 | 198 | // WorkspaceList contains a list of Workspace 199 | type WorkspaceList struct { 200 | metav1.TypeMeta `json:",inline"` 201 | metav1.ListMeta `json:"metadata,omitempty"` 202 | Items []Workspace `json:"items"` 203 | } 204 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/zz_generated.managed.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | 22 | // GetCondition of this Workspace. 23 | func (mg *Workspace) GetCondition(ct xpv1.ConditionType) xpv1.Condition { 24 | return mg.Status.GetCondition(ct) 25 | } 26 | 27 | // GetManagementPolicies of this Workspace. 28 | func (mg *Workspace) GetManagementPolicies() xpv1.ManagementPolicies { 29 | return mg.Spec.ManagementPolicies 30 | } 31 | 32 | // GetProviderConfigReference of this Workspace. 33 | func (mg *Workspace) GetProviderConfigReference() *xpv1.ProviderConfigReference { 34 | return mg.Spec.ProviderConfigReference 35 | } 36 | 37 | // GetWriteConnectionSecretToReference of this Workspace. 38 | func (mg *Workspace) GetWriteConnectionSecretToReference() *xpv1.LocalSecretReference { 39 | return mg.Spec.WriteConnectionSecretToReference 40 | } 41 | 42 | // SetConditions of this Workspace. 43 | func (mg *Workspace) SetConditions(c ...xpv1.Condition) { 44 | mg.Status.SetConditions(c...) 45 | } 46 | 47 | // SetManagementPolicies of this Workspace. 48 | func (mg *Workspace) SetManagementPolicies(r xpv1.ManagementPolicies) { 49 | mg.Spec.ManagementPolicies = r 50 | } 51 | 52 | // SetProviderConfigReference of this Workspace. 53 | func (mg *Workspace) SetProviderConfigReference(r *xpv1.ProviderConfigReference) { 54 | mg.Spec.ProviderConfigReference = r 55 | } 56 | 57 | // SetWriteConnectionSecretToReference of this Workspace. 58 | func (mg *Workspace) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretReference) { 59 | mg.Spec.WriteConnectionSecretToReference = r 60 | } 61 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/zz_generated.managedlist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 21 | 22 | // GetItems of this WorkspaceList. 23 | func (l *WorkspaceList) GetItems() []resource.Managed { 24 | items := make([]resource.Managed, len(l.Items)) 25 | for i := range l.Items { 26 | items[i] = &l.Items[i] 27 | } 28 | return items 29 | } 30 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/zz_generated.pc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | 22 | // GetCondition of this ClusterProviderConfig. 23 | func (p *ClusterProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition { 24 | return p.Status.GetCondition(ct) 25 | } 26 | 27 | // GetUsers of this ClusterProviderConfig. 28 | func (p *ClusterProviderConfig) GetUsers() int64 { 29 | return p.Status.Users 30 | } 31 | 32 | // SetConditions of this ClusterProviderConfig. 33 | func (p *ClusterProviderConfig) SetConditions(c ...xpv1.Condition) { 34 | p.Status.SetConditions(c...) 35 | } 36 | 37 | // SetUsers of this ClusterProviderConfig. 38 | func (p *ClusterProviderConfig) SetUsers(i int64) { 39 | p.Status.Users = i 40 | } 41 | 42 | // GetCondition of this ProviderConfig. 43 | func (p *ProviderConfig) GetCondition(ct xpv1.ConditionType) xpv1.Condition { 44 | return p.Status.GetCondition(ct) 45 | } 46 | 47 | // GetUsers of this ProviderConfig. 48 | func (p *ProviderConfig) GetUsers() int64 { 49 | return p.Status.Users 50 | } 51 | 52 | // SetConditions of this ProviderConfig. 53 | func (p *ProviderConfig) SetConditions(c ...xpv1.Condition) { 54 | p.Status.SetConditions(c...) 55 | } 56 | 57 | // SetUsers of this ProviderConfig. 58 | func (p *ProviderConfig) SetUsers(i int64) { 59 | p.Status.Users = i 60 | } 61 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/zz_generated.pcu.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" 21 | 22 | // GetProviderConfigReference of this ProviderConfigUsage. 23 | func (p *ProviderConfigUsage) GetProviderConfigReference() xpv1.ProviderConfigReference { 24 | return p.ProviderConfigReference 25 | } 26 | 27 | // GetResourceReference of this ProviderConfigUsage. 28 | func (p *ProviderConfigUsage) GetResourceReference() xpv1.TypedReference { 29 | return p.ResourceReference 30 | } 31 | 32 | // SetProviderConfigReference of this ProviderConfigUsage. 33 | func (p *ProviderConfigUsage) SetProviderConfigReference(r xpv1.ProviderConfigReference) { 34 | p.ProviderConfigReference = r 35 | } 36 | 37 | // SetResourceReference of this ProviderConfigUsage. 38 | func (p *ProviderConfigUsage) SetResourceReference(r xpv1.TypedReference) { 39 | p.ResourceReference = r 40 | } 41 | -------------------------------------------------------------------------------- /apis/namespaced/v1beta1/zz_generated.pculist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | // Code generated by angryjet. DO NOT EDIT. 17 | 18 | package v1beta1 19 | 20 | import resource "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 21 | 22 | // GetItems of this ProviderConfigUsageList. 23 | func (p *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage { 24 | items := make([]resource.ProviderConfigUsage, len(p.Items)) 25 | for i := range p.Items { 26 | items[i] = &p.Items[i] 27 | } 28 | return items 29 | } 30 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: provider-terraform 5 | description: "Terraform Provider for Crossplane" 6 | links: 7 | - url: https://marketplace.upbound.io/providers/upbound/provider-terraform/latest/docs/quickstart 8 | title: Provider terraform Quickstart 9 | annotations: 10 | github.com/project-slug: upbound/provider-terraform 11 | spec: 12 | type: service 13 | lifecycle: production 14 | owner: team-extensions 15 | -------------------------------------------------------------------------------- /cluster/images/provider-terraform/.gitconfig: -------------------------------------------------------------------------------- 1 | [credential] 2 | helper = store --file=$GIT_CRED_DIR/.git-credentials 3 | -------------------------------------------------------------------------------- /cluster/images/provider-terraform/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21.3 2 | RUN apk --no-cache add ca-certificates bash git curl 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | ENV TERRAFORM_VERSION=1.5.5 7 | ENV TF_IN_AUTOMATION=1 8 | ENV TF_PLUGIN_CACHE_DIR=/tf/plugin-cache 9 | 10 | ADD "bin/${TARGETOS}_${TARGETARCH}/provider" /usr/local/bin/crossplane-terraform-provider 11 | ADD .gitconfig .gitconfig 12 | 13 | # Do not change the URL from which the Terraform CLI is downloaded. 14 | # We are using an MPL-2.0 licensed version of the CLI and 15 | # we must make sure that this holds true. 16 | RUN curl -s -L https://github.com/upbound/terraform/releases/download/v${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_${TARGETOS}_${TARGETARCH}.zip -o terraform.zip \ 17 | && unzip -d /usr/local/bin terraform.zip \ 18 | && rm terraform.zip \ 19 | && chmod +x /usr/local/bin/terraform \ 20 | && mkdir -p ${TF_PLUGIN_CACHE_DIR} \ 21 | && chown -R 2000 /tf 22 | # As of Crossplane v1.3.0 provider controllers run as UID 2000. 23 | # https://github.com/crossplane/crossplane/blob/v1.3.0/internal/controller/pkg/revision/deployment.go#L32 24 | 25 | USER 65532 26 | ENTRYPOINT ["crossplane-terraform-provider"] 27 | -------------------------------------------------------------------------------- /cluster/images/provider-terraform/Makefile: -------------------------------------------------------------------------------- 1 | # ==================================================================================== 2 | # Setup Project 3 | 4 | include ../../../build/makelib/common.mk 5 | 6 | # ==================================================================================== 7 | # Options 8 | 9 | include ../../../build/makelib/imagelight.mk 10 | 11 | # ==================================================================================== 12 | # Targets 13 | 14 | img.build: 15 | @$(INFO) docker build $(BUILD_ARGS) $(IMAGE) 16 | @$(MAKE) BUILD_ARGS="$(BUILD_ARGS) --load" img.build.shared 17 | @$(OK) docker build $(IMAGE) 18 | 19 | img.publish: 20 | @$(INFO) Skipping image publish for $(IMAGE) 21 | @echo Publish is deferred to xpkg machinery 22 | @$(OK) Image publish skipped for $(IMAGE) 23 | 24 | img.build.shared: 25 | @cp Dockerfile $(IMAGE_TEMP_DIR) || $(FAIL) 26 | @cp .gitconfig $(IMAGE_TEMP_DIR) || $(FAIL) 27 | @cp -r $(OUTPUT_DIR)/bin/ $(IMAGE_TEMP_DIR)/bin || $(FAIL) 28 | @docker buildx build $(BUILD_ARGS) \ 29 | --platform $(IMAGE_PLATFORMS) \ 30 | -t $(IMAGE) \ 31 | $(IMAGE_TEMP_DIR) || $(FAIL) 32 | 33 | img.promote: 34 | @$(INFO) Skipping image promotion from $(FROM_IMAGE) to $(TO_IMAGE) 35 | @echo Promote is deferred to xpkg machinery 36 | @$(OK) Image promotion skipped for $(FROM_IMAGE) to $(TO_IMAGE) 37 | -------------------------------------------------------------------------------- /cluster/test/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -aeuo pipefail 3 | 4 | scriptdir="$( dirname "${BASH_SOURCE[0]}")" 5 | 6 | echo "Running setup.sh" 7 | 8 | if [[ -n "${UPTEST_CLOUD_CREDENTIALS:-}" ]]; then 9 | # NOTE(turkenh): UPTEST_CLOUD_CREDENTIALS may contain more than one cloud credentials that we expect to be provided 10 | # in a single GitHub secret. We expect them provided as key=value pairs separated by newlines. Currently we expect 11 | # AWS and GCP credentials to be provided. For example: 12 | # AWS='[default] 13 | # aws_access_key_id = REDACTED 14 | # aws_secret_access_key = REDACTED' 15 | # GCP='{ 16 | # "type": "service_account", 17 | # "project_id": "REDACTED", 18 | # "private_key_id": "REDACTED", 19 | # "private_key": "-----BEGIN PRIVATE KEY-----\nREDACTED\n-----END PRIVATE KEY-----\n", 20 | # "client_email": "REDACTED", 21 | # "client_id": "REDACTED", 22 | # "auth_uri": "https://accounts.google.com/o/oauth2/auth", 23 | # "token_uri": "https://oauth2.googleapis.com/token", 24 | # "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 25 | # "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/official-provider-testing%40official-provider-testing.iam.gserviceaccount.com" 26 | # }' 27 | eval "${UPTEST_CLOUD_CREDENTIALS}" 28 | 29 | if [[ -n "${AWS:-}" ]]; then 30 | echo "Creating cloud credentials secret for AWS..." 31 | ${KUBECTL} -n upbound-system create secret generic aws-creds --from-literal=credentials="${AWS}" --dry-run=client -o yaml | ${KUBECTL} apply -f - 32 | ${KUBECTL} apply -f "${scriptdir}/../../examples/cluster/providerconfig-aws.yaml" 33 | ${KUBECTL} apply -f "${scriptdir}/../../examples/namespaced/clusterproviderconfig-aws.yaml" 34 | fi 35 | 36 | if [[ -n "${GCP:-}" ]]; then 37 | echo "Creating cloud credentials secret for GCP..." 38 | ${KUBECTL} -n upbound-system create secret generic gcp-creds --from-literal=credentials="${GCP}" --dry-run=client -o yaml | ${KUBECTL} apply -f - 39 | ${KUBECTL} apply -f "${scriptdir}/../../examples/cluster/providerconfig.yaml" 40 | ${KUBECTL} apply -f "${scriptdir}/../../examples/namespaced/clusterproviderconfig.yaml" 41 | fi 42 | fi -------------------------------------------------------------------------------- /docs/monolith/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Troubleshooting Reconciliation Issues 3 | weight: 3 4 | --- 5 | # Troubleshooting Reconciliation Issues 6 | 7 | This document provides guidance for troubleshooting common reconciliation and performance issues with the Terraform provider, based on real-world scenarios and their resolutions. 8 | 9 | ## Reconciliations Blocking Unexpectedly 10 | 11 | ### Problem Description 12 | 13 | You may experience reconciliations backing up behind long-running terraform apply/destroy operations, where workspaces appear to get "stuck" even when CPU resources are available. This manifests as: 14 | 15 | - Reconciliations not making progress despite available CPU capacity 16 | - Long queues of pending reconciliations 17 | - Underutilization of configured `--max-reconcile-rate` settings 18 | 19 | ### Root Cause 20 | 21 | This issue is caused by the provider's locking mechanism used to prevent terraform plugin cache corruption: 22 | 23 | 1. **Read-Write Lock Behavior**: The provider uses RWMutex for workspace operations: 24 | - Multiple `terraform plan` operations can run concurrently (RLock) 25 | - Only one `terraform init` operation can run at a time (Lock) 26 | - When a Lock is requested, it blocks all new RLock requests until completed 27 | 28 | 2. **Blocking Scenario**: When a new Workspace is created requiring `terraform init`: 29 | - The Lock request waits for all current RLock (plan) operations to finish 30 | - Meanwhile, all new RLock requests are blocked 31 | - This effectively makes the provider single-threaded until the init completes 32 | 33 | ### Solutions and Workarounds 34 | 35 | #### 1. Use Persistent Storage (Recommended) 36 | 37 | Mount a persistent volume to `/tf` to eliminate the need for frequent `terraform init` operations: 38 | 39 | ```yaml 40 | apiVersion: pkg.crossplane.io/v1alpha1 41 | kind: DeploymentRuntimeConfig 42 | metadata: 43 | name: provider-terraform-with-pv 44 | spec: 45 | deploymentTemplate: 46 | spec: 47 | template: 48 | spec: 49 | containers: 50 | - name: package-runtime 51 | volumeMounts: 52 | - name: tf-workspace 53 | mountPath: /tf 54 | volumes: 55 | - name: tf-workspace 56 | persistentVolumeClaim: 57 | claimName: provider-terraform-pvc 58 | ``` 59 | 60 | **Benefits:** 61 | - Workspaces persist across pod restarts 62 | - Eliminates need to re-run `terraform init` on restart 63 | - Reduces plugin download traffic 64 | - Significantly improves performance with many workspaces 65 | 66 | #### 2. Disable Plugin Cache for High Concurrency 67 | 68 | If persistent storage is not available, consider disabling the terraform plugin cache to avoid locking entirely: 69 | 70 | ```yaml 71 | apiVersion: pkg.crossplane.io/v1alpha1 72 | kind: ControllerConfig 73 | metadata: 74 | name: provider-terraform-no-cache 75 | spec: 76 | args: 77 | - --debug 78 | env: 79 | - name: TF_PLUGIN_CACHE_DIR 80 | value: "" 81 | ``` 82 | 83 | **Trade-offs:** 84 | - Eliminates blocking issues 85 | - Increases network traffic (providers downloaded per workspace) 86 | - Higher NAT gateway costs in cloud environments 87 | - Still better than single-threaded performance 88 | 89 | #### 3. Optimize Concurrency Settings 90 | 91 | Align your `--max-reconcile-rate` with available CPU resources: 92 | 93 | ```yaml 94 | apiVersion: pkg.crossplane.io/v1alpha1 95 | kind: ControllerConfig 96 | metadata: 97 | name: provider-terraform-optimized 98 | spec: 99 | args: 100 | - --max-reconcile-rate=4 # Match your CPU allocation 101 | resources: 102 | requests: 103 | cpu: 4 104 | limits: 105 | cpu: 4 106 | ``` 107 | 108 | ### Monitoring and Diagnosis 109 | 110 | Use these Prometheus queries to monitor reconciliation performance: 111 | 112 | ```promql 113 | # Maximum concurrent reconciles configured 114 | sum by (controller)(controller_runtime_max_concurrent_reconciles{controller="managed/workspace.tf.upbound.io"}) 115 | 116 | # Active workers currently processing 117 | sum by (controller)(controller_runtime_active_workers{controller="managed/workspace.tf.upbound.io"}) 118 | 119 | # Reconciliation rate 120 | sum by (controller)(rate(controller_runtime_reconcile_total{controller="managed/workspace.tf.upbound.io"}[5m])) 121 | 122 | # CPU usage 123 | sum by ()(rate(container_cpu_usage_seconds_total{container!="",namespace="crossplane-system",pod=~"upbound-provider-terraform.*"}[5m])) 124 | 125 | # Memory usage 126 | sum by ()(container_memory_working_set_bytes{container!="",namespace="crossplane-system",pod=~"upbound-provider-terraform.*"}) 127 | ``` 128 | 129 | ## Remote Git Repository Issues 130 | 131 | ### Problem Description 132 | 133 | When using remote git repositories as workspace sources, you may experience: 134 | 135 | - Excessive network traffic 136 | - Providers being re-downloaded on every reconciliation 137 | - "text file busy" errors even with persistent volumes 138 | 139 | ### Root Cause 140 | 141 | The provider removes and recreates the entire workspace directory for each reconciliation when using remote repositories due to limitations in the go-getter library. 142 | 143 | ### Current Limitations 144 | 145 | - Remote repositories are re-cloned on every reconciliation 146 | - `terraform init` runs on every reconciliation for git-backed workspaces 147 | - Plugin cache conflicts can still occur during rapid workspace creation 148 | 149 | ### Recommendations 150 | 151 | 1. **Use Inline Workspaces**: When possible, embed terraform configuration directly in the Workspace spec rather than referencing remote repositories. 152 | 2. **Disable Plugin Cache**: For remote repositories with high reconciliation rates, disable the plugin cache to avoid conflicts. 153 | 3. **Monitor Traffic Costs**: Be aware of increased network egress costs when using remote repositories with disabled plugin cache. 154 | 155 | ## Error Messages and Recovery 156 | 157 | ### "text file busy" Errors 158 | 159 | ``` 160 | Error: Failed to install provider 161 | Error while installing hashicorp/aws v5.44.0: open 162 | /tf/plugin-cache/registry.terraform.io/hashicorp/aws/5.44.0/linux_arm64/terraform-provider-aws_v5.44.0_x5: 163 | text file busy 164 | ``` 165 | 166 | **Resolution**: These errors typically resolve automatically due to built-in retry logic, but indicate plugin cache conflicts. Consider: 167 | - Using persistent volumes with plugin cache disabled 168 | - Reducing `--max-reconcile-rate` during initial workspace creation 169 | 170 | ### CLI Configuration Warnings 171 | 172 | ``` 173 | Warning: Unable to open CLI configuration file 174 | The CLI configuration file at "./.terraformrc" does not exist. 175 | ``` 176 | 177 | **Resolution**: This is typically harmless but can be resolved by: 178 | - Mounting a custom `.terraformrc` configuration 179 | - Setting appropriate terraform CLI environment variables 180 | 181 | ## Best Practices 182 | 183 | 1. **Start Conservative**: Begin with `--max-reconcile-rate=1` and increase gradually while monitoring performance. 184 | 2. **Match Resources**: Ensure CPU requests/limits align with your concurrency settings. 185 | 3. **Use Persistent Storage**: Always use persistent volumes in production environments with multiple workspaces. 186 | 4. **Monitor Actively**: Set up monitoring for reconciliation rates, error rates, and resource utilization. 187 | 5. **Plan for Scale**: Consider the total number of workspaces and their reconciliation patterns when designing your deployment. 188 | -------------------------------------------------------------------------------- /docs/monolith/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Quickstart", 4 | "location": "Quickstart.md" 5 | }, 6 | { 7 | "name": "Configuration", 8 | "location": "Configuration.md" 9 | }, 10 | { 11 | "name": "Migration Guide", 12 | "location": "MigrationGuide.md" 13 | } 14 | ] -------------------------------------------------------------------------------- /examples/cluster/.terraformrc: -------------------------------------------------------------------------------- 1 | provider_installation { 2 | filesystem_mirror { 3 | path = "/usr/share/terraform/providers" 4 | include = ["registry.terraform.io/hashicorp/*"] 5 | } 6 | direct { 7 | exclude = ["example.com/*/*"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/cluster/aws-eks-irsa-setup.yaml: -------------------------------------------------------------------------------- 1 | # NOTE: You need a trust policy associated with the role matching the Service Account and OIDC provider 2 | # Something like the following: 3 | # { 4 | # "Version": "2012-10-17", 5 | # "Statement": [ 6 | # { 7 | # "Effect": "Allow", 8 | # "Principal": { 9 | # "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}" 10 | # }, 11 | # "Action": "sts:AssumeRoleWithWebIdentity", 12 | # "Condition": { 13 | # "StringLike": { 14 | # "${OIDC_PROVIDER}:sub": "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:provider-terraform-*" 15 | # } 16 | # } 17 | # } 18 | # ] 19 | # } 20 | # 21 | # Where: 22 | # OIDC_PROVIDER=$(aws eks describe-cluster --name "${CLUSTER_NAME}" --region "${AWS_REGION}" --query "cluster.identity.oidc.issuer" --output text | sed -e "s/^https:\/\///") 23 | --- 24 | apiVersion: pkg.crossplane.io/v1alpha1 25 | kind: ControllerConfig 26 | metadata: 27 | name: terraform-config 28 | labels: 29 | app: crossplane-provider-terraform 30 | annotations: 31 | eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNTID:role/provider-terraform 32 | spec: 33 | image: crossplane/provider-terraform-controller:v0.2.0 34 | args: 35 | - "--debug" 36 | podSecurityContext: 37 | fsGroup: 2000 38 | --- 39 | apiVersion: pkg.crossplane.io/v1 40 | kind: Provider 41 | metadata: 42 | name: crossplane-provider-terraform 43 | spec: 44 | package: crossplane/provider-terraform:v0.2.0 45 | controllerConfigRef: 46 | name: terraform-config 47 | --- 48 | apiVersion: tf.upbound.io/v1beta1 49 | kind: ProviderConfig 50 | metadata: 51 | annotations: {} 52 | name: aws-irsa 53 | spec: 54 | configuration: | 55 | terraform { 56 | backend "kubernetes" { 57 | secret_suffix = "providerconfig-tf-aws" 58 | namespace = "crossplane-system" 59 | in_cluster_config = true 60 | } 61 | } 62 | provider "aws" {} 63 | credentials: 64 | # Terraform natively supports IRSA auth so we can use None here 65 | - filename: aws.json 66 | source: None 67 | -------------------------------------------------------------------------------- /examples/cluster/creationblocker/README.md: -------------------------------------------------------------------------------- 1 | # Blocking condition PoC 2 | 3 | A very special edge case workaround, use it at your own risk :) 4 | 5 | This example Configuration(Composition+XRD) demonstrates a blocking condition 6 | PoC for blocking the resource creation according to the result of some special 7 | condition. 8 | 9 | We use provider-terraform Workspace to create the special condition and check if 10 | VPC alredy exists and in case it does it fully blocks the further execution by 11 | intentionally violating the RFC1123 in metada.name of composed VPC resource. 12 | -------------------------------------------------------------------------------- /examples/cluster/creationblocker/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: xsubnets.aws.platformref.upbound.io 5 | labels: 6 | provider: aws 7 | spec: 8 | compositeTypeRef: 9 | apiVersion: aws.platformref.upbound.io/v1alpha1 10 | kind: XSubnet 11 | resources: 12 | - name: vpc-data-source 13 | base: 14 | apiVersion: tf.upbound.io/v1beta1 15 | kind: Workspace 16 | spec: 17 | forProvider: 18 | source: Inline 19 | module: | 20 | data "aws_vpcs" "exists" { 21 | tags = { 22 | Name = var.vpcName 23 | } 24 | } 25 | output "vpc_exists" { 26 | description = "VPC name" 27 | value = length(data.aws_vpcs.exists.ids) > 0 ? "-block.me" : "blockmenot" 28 | } 29 | 30 | variable "vpcName" { 31 | description = "VPC name" 32 | type = string 33 | } 34 | vars: 35 | - key: vpcName 36 | patches: 37 | - fromFieldPath: spec.vpcName 38 | toFieldPath: spec.forProvider.vars[0].value 39 | - type: ToCompositeFieldPath 40 | fromFieldPath: status.atProvider.outputs.vpc_exists 41 | toFieldPath: status.block 42 | policy: 43 | fromFieldPath: Optional 44 | - name: vpc-blocked 45 | base: 46 | apiVersion: ec2.aws.upbound.io/v1beta1 47 | kind: VPC 48 | spec: 49 | forProvider: 50 | region: eu-central-1 51 | cidrBlock: 10.0.0.0/24 52 | patches: 53 | - type: CombineFromComposite 54 | combine: 55 | variables: 56 | - fromFieldPath: status.block 57 | - fromFieldPath: spec.vpcName 58 | strategy: string 59 | string: 60 | fmt: "%s%s" 61 | toFieldPath: metadata.name 62 | policy: 63 | fromFieldPath: Required 64 | - name: subnet 65 | base: 66 | apiVersion: ec2.aws.upbound.io/v1beta1 67 | kind: Subnet 68 | spec: 69 | forProvider: 70 | region: eu-central-1 71 | cidrBlock: 10.0.0.0/25 72 | vpcIdSelector: 73 | matchControllerRef: true 74 | -------------------------------------------------------------------------------- /examples/cluster/creationblocker/definition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: CompositeResourceDefinition 3 | metadata: 4 | name: xsubnets.aws.platformref.upbound.io 5 | spec: 6 | group: aws.platformref.upbound.io 7 | names: 8 | kind: XSubnet 9 | plural: xsubnets 10 | versions: 11 | - name: v1alpha1 12 | served: true 13 | referenceable: true 14 | schema: 15 | openAPIV3Schema: 16 | type: object 17 | properties: 18 | spec: 19 | type: object 20 | properties: 21 | vpcName: 22 | type: string 23 | required: 24 | - vpcName 25 | status: 26 | description: A Status represents the observed state 27 | properties: 28 | block: 29 | description: block arbitrary resource creation 30 | type: string 31 | #default: -blockcreation 32 | type: object 33 | -------------------------------------------------------------------------------- /examples/cluster/creationblocker/xsubnetblocking.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: aws.platformref.upbound.io/v1alpha1 2 | kind: XSubnet 3 | metadata: 4 | name: subnet-with-blocking-vpc 5 | spec: 6 | vpcName: blockingvpc 7 | -------------------------------------------------------------------------------- /examples/cluster/creationblocker/xsubnetnotblocking.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: aws.platformref.upbound.io/v1alpha1 2 | kind: XSubnet 3 | metadata: 4 | name: subnet-with-non-blocking-vpc 5 | spec: 6 | vpcName: nonblockingvpc 7 | -------------------------------------------------------------------------------- /examples/cluster/environment/envconfigmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | envfromconfigmap: EnvFromConfigMapValue 4 | kind: ConfigMap 5 | metadata: 6 | name: provider-terraform-test 7 | namespace: default 8 | -------------------------------------------------------------------------------- /examples/cluster/environment/envsecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | envfromsecret: c3VwZXJzZWNyZXQ= 4 | kind: Secret 5 | metadata: 6 | name: provider-terraform-test 7 | namespace: default 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /examples/cluster/environment/workspace-inline-env-aws.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: sample-inline-with-env 5 | spec: 6 | forProvider: 7 | source: Inline 8 | env: 9 | - name: TF_VAR_vpcName 10 | value: 'sample-tf-inline-with-env' 11 | - name: TF_VAR_EnvFromConfigMap 12 | configMapKeyRef: 13 | namespace: default 14 | name: provider-terraform-test 15 | key: envfromconfigmap 16 | - name: TF_VAR_EnvFromSecret 17 | secretKeyRef: 18 | namespace: default 19 | name: provider-terraform-test 20 | key: envfromsecret 21 | module: | 22 | resource "aws_vpc" "main" { 23 | cidr_block = "10.0.0.0/16" 24 | tags = { 25 | Name = var.vpcName 26 | EnvFromConfigMap = var.EnvFromConfigMap 27 | EnvFromSecret = var.EnvFromSecret 28 | } 29 | } 30 | resource "aws_subnet" "main" { 31 | vpc_id = aws_vpc.main.id 32 | cidr_block = "10.0.1.0/24" 33 | } 34 | output "vpc_id" { 35 | value = aws_vpc.main.id 36 | } 37 | output "subnet_data" { 38 | value = { 39 | "id" = aws_subnet.main.id 40 | "arn" = aws_subnet.main.arn 41 | } 42 | } 43 | variable "vpcName" { 44 | description = "VPC name" 45 | type = string 46 | } 47 | variable "EnvFromConfigMap" { 48 | description = "Environment Value From ConfigMap" 49 | type = string 50 | } 51 | variable "EnvFromSecret" { 52 | description = "Environment Value From Secret" 53 | type = string 54 | } 55 | -------------------------------------------------------------------------------- /examples/cluster/importer/README.md: -------------------------------------------------------------------------------- 1 | # Workaround for Importing Resources Functionality 2 | 3 | This example Configuration(Composition+XRD) demonstrates a temporary workaround 4 | for Importing Resources functionality before it is fully automated in the core Crossplane. 5 | 6 | In a nutshell it is a well-known 7 | https://docs.crossplane.io/knowledge-base/guides/import-existing-resources/#import-resources-manually 8 | process that is automated via the Composition. 9 | 10 | The workaround consists of a `Composition` that provides a mix of provider-terraform 11 | `Workspace` with the 12 | [aws_vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) 13 | **data** resource as Inline module. 14 | 15 | It publishes the discovered imported `vpcdata` to the `XSubnet` XR status. 16 | 17 | The `vpcdata.id` and other data from the status is getting eventually consumed by `VPC` 18 | provider-aws resource following which is a part of the same `Composition`. `VPC` 19 | is eventually used as a reference in a new standard `Subnet` creation. 20 | -------------------------------------------------------------------------------- /examples/cluster/importer/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: xsubnets.aws.platformref.upbound.io 5 | labels: 6 | provider: aws 7 | spec: 8 | compositeTypeRef: 9 | apiVersion: aws.platformref.upbound.io/v1alpha1 10 | kind: XSubnet 11 | resources: 12 | - name: vpc-data-source 13 | base: 14 | apiVersion: tf.upbound.io/v1beta1 15 | kind: Workspace 16 | spec: 17 | forProvider: 18 | source: Inline 19 | module: | 20 | data "aws_vpc" "import" { 21 | tags = { 22 | Name = var.vpcName 23 | } 24 | } 25 | output "vpc_data" { 26 | description = "Imported VPC data" 27 | value = { 28 | "id" = try(data.aws_vpc.import.id, "") 29 | "cidrBlock" = try(data.aws_vpc.import.cidr_block, "") 30 | "enableDnsSupport" = try(data.aws_vpc.import.enable_dns_support, "") 31 | "enableDnsHostnames" = try(data.aws_vpc.import.enable_dns_hostnames, "") 32 | } 33 | } 34 | variable "vpcName" { 35 | description = "VPC name" 36 | type = string 37 | } 38 | vars: 39 | - key: vpcName 40 | patches: 41 | - fromFieldPath: spec.vpcName 42 | toFieldPath: spec.forProvider.vars[0].value 43 | - type: ToCompositeFieldPath 44 | fromFieldPath: status.atProvider.outputs.vpc_data 45 | toFieldPath: status.share.vpcData 46 | policy: 47 | fromFieldPath: Optional 48 | - name: vpc-imported 49 | base: 50 | apiVersion: ec2.aws.upbound.io/v1beta1 51 | kind: VPC 52 | spec: 53 | deletionPolicy: Orphan 54 | forProvider: 55 | region: eu-central-1 56 | patches: 57 | - fromFieldPath: spec.vpcName 58 | toFieldPath: spec.forProvider.tags.Name 59 | - fromFieldPath: status.share.vpcData.id 60 | toFieldPath: metadata.annotations[crossplane.io/external-name] 61 | policy: 62 | fromFieldPath: Required 63 | - fromFieldPath: status.share.vpcData.cidrBlock 64 | toFieldPath: spec.forProvider.cidrBlock 65 | policy: 66 | fromFieldPath: Required 67 | - fromFieldPath: status.share.vpcData.enableDnsSupport 68 | toFieldPath: spec.forProvider.enableDnsSupport 69 | policy: 70 | fromFieldPath: Required 71 | - fromFieldPath: status.share.vpcData.enableDnsHostnames 72 | toFieldPath: spec.forProvider.enableDnsHostnames 73 | policy: 74 | fromFieldPath: Required 75 | - name: subnet 76 | base: 77 | apiVersion: ec2.aws.upbound.io/v1beta1 78 | kind: Subnet 79 | spec: 80 | forProvider: 81 | region: eu-central-1 82 | cidrBlock: 10.0.0.0/25 83 | vpcIdSelector: 84 | matchControllerRef: true 85 | -------------------------------------------------------------------------------- /examples/cluster/importer/definition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: CompositeResourceDefinition 3 | metadata: 4 | name: xsubnets.aws.platformref.upbound.io 5 | spec: 6 | group: aws.platformref.upbound.io 7 | names: 8 | kind: XSubnet 9 | plural: xsubnets 10 | versions: 11 | - name: v1alpha1 12 | served: true 13 | referenceable: true 14 | schema: 15 | openAPIV3Schema: 16 | type: object 17 | properties: 18 | spec: 19 | type: object 20 | properties: 21 | vpcName: 22 | type: string 23 | required: 24 | - vpcName 25 | status: 26 | description: A Status represents the observed state 27 | properties: 28 | share: 29 | description: Freeform field containing status information 30 | type: object 31 | x-kubernetes-preserve-unknown-fields: true 32 | type: object 33 | -------------------------------------------------------------------------------- /examples/cluster/importer/xsubnet.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: aws.platformref.upbound.io/v1alpha1 2 | kind: XSubnet 3 | metadata: 4 | name: subnet-with-imported-vpc 5 | spec: 6 | vpcName: importvpc 7 | -------------------------------------------------------------------------------- /examples/cluster/install.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: pkg.crossplane.io/v1alpha1 3 | kind: ControllerConfig 4 | metadata: 5 | name: terraform-config 6 | labels: 7 | app: crossplane-provider-terraform 8 | spec: 9 | args: ["-d"] 10 | --- 11 | apiVersion: pkg.crossplane.io/v1 12 | kind: Provider 13 | metadata: 14 | name: crossplane-provider-terraform 15 | spec: 16 | package: xpkg.upbound.io/upbound/provider-terraform:v0.2.0 17 | controllerConfigRef: 18 | name: terraform-config 19 | -------------------------------------------------------------------------------- /examples/cluster/observe-only-composition/README.md: -------------------------------------------------------------------------------- 1 | # Workaround for Observe-Only Resources Functionality 2 | 3 | This example Configuration(Composition+XRD) demonstrates a temporary workaround 4 | for Observe-Only Resources functionality before it is [properly 5 | implemented](https://github.com/crossplane/crossplane/issues/1722) 6 | the core Crossplane. 7 | 8 | The workaround consists of a `Composition` that provides a mix of provider-terraform 9 | `Workspace` with the 10 | [aws_vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) 11 | **data** resource as Inline module. 12 | 13 | It publishes the discovered observe-only `vpcId` to the `XSubnet` XR status. 14 | 15 | The `vpcId` from the status is getting eventually consumed by the native `Subnet` 16 | provider-aws resource which is a part of the same `Composition`. 17 | -------------------------------------------------------------------------------- /examples/cluster/observe-only-composition/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: xsubnets.aws.platformref.upbound.io 5 | labels: 6 | provider: aws 7 | spec: 8 | compositeTypeRef: 9 | apiVersion: aws.platformref.upbound.io/v1alpha1 10 | kind: XSubnet 11 | resources: 12 | - name: observe-only-vpc 13 | base: 14 | apiVersion: tf.upbound.io/v1beta1 15 | kind: Workspace 16 | metadata: 17 | name: observe-only-vpc 18 | spec: 19 | forProvider: 20 | source: Inline 21 | module: | 22 | data "aws_vpc" "observe_only" { 23 | tags = { 24 | Name = var.vpcName 25 | } 26 | } 27 | output "vpc_id" { 28 | description = "Observe Only VPC ID" 29 | value = try(data.aws_vpc.observe_only.id, "") 30 | } 31 | variable "vpcName" { 32 | description = "VPC name" 33 | type = string 34 | } 35 | vars: 36 | - key: vpcName 37 | patches: 38 | - fromFieldPath: spec.vpcName 39 | toFieldPath: spec.forProvider.vars[0].value 40 | - type: ToCompositeFieldPath 41 | fromFieldPath: status.atProvider.outputs.vpc_id 42 | toFieldPath: status.share.vpcId 43 | policy: 44 | fromFieldPath: Optional 45 | - name: subnet 46 | base: 47 | apiVersion: ec2.aws.upbound.io/v1beta1 48 | kind: Subnet 49 | spec: 50 | forProvider: 51 | region: eu-central-1 52 | cidrBlock: 10.0.0.0/25 53 | patches: 54 | - fromFieldPath: status.share.vpcId 55 | toFieldPath: spec.forProvider.vpcId 56 | policy: 57 | fromFieldPath: Required 58 | -------------------------------------------------------------------------------- /examples/cluster/observe-only-composition/definition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: CompositeResourceDefinition 3 | metadata: 4 | name: xsubnets.aws.platformref.upbound.io 5 | spec: 6 | group: aws.platformref.upbound.io 7 | names: 8 | kind: XSubnet 9 | plural: xsubnets 10 | versions: 11 | - name: v1alpha1 12 | served: true 13 | referenceable: true 14 | schema: 15 | openAPIV3Schema: 16 | type: object 17 | properties: 18 | spec: 19 | type: object 20 | properties: 21 | vpcName: 22 | type: string 23 | required: 24 | - vpcName 25 | status: 26 | description: A Status represents the observed state 27 | properties: 28 | share: 29 | description: Freeform field containing status information 30 | type: object 31 | x-kubernetes-preserve-unknown-fields: true 32 | type: object 33 | -------------------------------------------------------------------------------- /examples/cluster/observe-only-composition/xsubnet.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: aws.platformref.upbound.io/v1alpha1 2 | kind: XSubnet 3 | metadata: 4 | name: subnet-with-observe-only-vpc 5 | spec: 6 | vpcName: observeonly 7 | -------------------------------------------------------------------------------- /examples/cluster/providerconfig-aws.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tf.upbound.io/v1beta1 3 | kind: ProviderConfig 4 | metadata: 5 | name: aws-eu-west-1 6 | spec: 7 | credentials: 8 | - filename: aws-creds.ini 9 | source: Secret 10 | secretRef: 11 | namespace: upbound-system 12 | name: aws-creds 13 | key: credentials 14 | configuration: | 15 | terraform { 16 | required_providers { 17 | aws = { 18 | source = "hashicorp/aws" 19 | version = "5.6.1" 20 | } 21 | } 22 | backend "kubernetes" { 23 | secret_suffix = "providerconfig-aws-eu-west-1" 24 | namespace = "upbound-system" 25 | in_cluster_config = true 26 | } 27 | } 28 | provider "aws" { 29 | shared_credentials_files = ["${path.module}/aws-creds.ini"] 30 | region = "us-east-2" 31 | } 32 | -------------------------------------------------------------------------------- /examples/cluster/providerconfig-azure.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: azure-creds 6 | type: Opaque 7 | stringData: 8 | "credentials": |- 9 | { 10 | "clientId": "wwwww~wwwwwwwww", 11 | "clientSecret": "xxxx-xxxx-xxxx-xxxx-xxxx", 12 | "tenantId": "yyyy-yyyy-yyyy-yyyy-yyyy", 13 | "subscriptionId": "zzzz-zzzz-zzzz-zzzz-zzzz" 14 | } 15 | --- 16 | apiVersion: tf.upbound.io/v1beta1 17 | kind: ProviderConfig 18 | metadata: 19 | name: azure-westeurope 20 | spec: 21 | credentials: 22 | # Filename has to comply with below naming convention: 23 | # - Files named exactly terraform.tfvars or terraform.tfvars.json. 24 | # - Any files with names ending in .auto.tfvars or .auto.tfvars.json. 25 | - filename: terraform.tfvars.json 26 | source: Secret 27 | secretRef: 28 | namespace: upbound-system 29 | name: azure-creds 30 | key: credentials 31 | configuration: | 32 | terraform { 33 | required_providers { 34 | azurerm = { 35 | source = "hashicorp/azurerm" 36 | version = "3.78.0" 37 | } 38 | } 39 | 40 | backend "kubernetes" { 41 | secret_suffix = "providerconfig-azure-westeurope" 42 | namespace = "upbound-system" 43 | in_cluster_config = true 44 | } 45 | } 46 | 47 | variable "subscriptionId" { 48 | type = string 49 | } 50 | 51 | variable "tenantId" { 52 | type = string 53 | } 54 | 55 | variable "clientId" { 56 | type = string 57 | } 58 | 59 | variable "clientSecret" { 60 | type = string 61 | } 62 | 63 | provider "azurerm" { 64 | subscription_id = var.subscriptionId 65 | tenant_id = var.tenantId 66 | client_id = var.clientId 67 | client_secret = var.clientSecret 68 | features {} 69 | } -------------------------------------------------------------------------------- /examples/cluster/providerconfig-backend-file.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: ProviderConfig 3 | metadata: 4 | name: default 5 | spec: 6 | # Note that unlike most provider configs this one supports an array of 7 | # credentials. This is because each Terraform workspace uses a single 8 | # Crossplane provider config, but could use multiple Terraform providers each 9 | # with their own credentials. 10 | credentials: 11 | - filename: gcp-credentials.json 12 | source: Secret 13 | secretRef: 14 | namespace: upbound-system 15 | name: gcp-creds 16 | key: credentials 17 | # This optional configuration block can be used to inject HCL into any 18 | # workspace that uses this provider config, for example to setup Terraform 19 | # providers. 20 | configuration: | 21 | provider "google" { 22 | credentials = "gcp-credentials.json" 23 | project = "official-provider-testing" 24 | } 25 | 26 | // Defining partial backend configuration as documented at 27 | // https://developer.hashicorp.com/terraform/language/settings/backends/configuration#partial-configuration 28 | terraform { 29 | backend "kubernetes" {} 30 | } 31 | # Using backend configuration file as documented at 32 | # https://developer.hashicorp.com/terraform/language/settings/backends/configuration#file 33 | backendFile: | 34 | secret_suffix = "providerconfig-default" 35 | namespace = "upbound-system" 36 | in_cluster_config = true -------------------------------------------------------------------------------- /examples/cluster/providerconfig-terraformrc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: ProviderConfig 3 | metadata: 4 | name: default 5 | spec: 6 | credentials: 7 | - filename: aws-creds.ini 8 | source: Secret 9 | secretRef: 10 | namespace: upbound-system 11 | name: aws-creds 12 | key: credentials 13 | # kubectl -n upbound-system create secret generic terraformrc --from-file=examples/.terraformrc 14 | - filename: .terraformrc 15 | source: Secret 16 | secretRef: 17 | namespace: upbound-system 18 | name: terraformrc 19 | key: .terraformrc 20 | configuration: | 21 | terraform { 22 | backend "kubernetes" { 23 | secret_suffix = "providerconfig-aws-eu-west-1" 24 | namespace = "upbound-system" 25 | in_cluster_config = true 26 | } 27 | } 28 | provider "aws" { 29 | shared_credentials_file = "${path.module}/aws-creds.ini" 30 | region = "eu-west-1" 31 | } 32 | -------------------------------------------------------------------------------- /examples/cluster/providerconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: ProviderConfig 3 | metadata: 4 | name: default 5 | spec: 6 | # Note that unlike most provider configs this one supports an array of 7 | # credentials. This is because each Terraform workspace uses a single 8 | # Crossplane provider config, but could use multiple Terraform providers each 9 | # with their own credentials. 10 | credentials: 11 | - filename: gcp-credentials.json 12 | source: Secret 13 | secretRef: 14 | namespace: upbound-system 15 | name: gcp-creds 16 | key: credentials 17 | # This optional configuration block can be used to inject HCL into any 18 | # workspace that uses this provider config, for example to setup Terraform 19 | # providers. 20 | configuration: | 21 | provider "google" { 22 | credentials = "gcp-credentials.json" 23 | project = "official-provider-testing" 24 | } 25 | 26 | // Modules _must_ use remote state. The provider does not persist state. 27 | terraform { 28 | backend "kubernetes" { 29 | secret_suffix = "providerconfig-default" 30 | namespace = "upbound-system" 31 | in_cluster_config = true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/cluster/transition/00-mr-tf-workspace/workspace-inline.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: sample-inline 5 | spec: 6 | forProvider: 7 | source: Inline 8 | module: | 9 | resource "aws_vpc" "main" { 10 | cidr_block = "10.0.0.0/16" 11 | tags = { 12 | Name = var.vpcName 13 | } 14 | } 15 | resource "aws_subnet" "main" { 16 | vpc_id = aws_vpc.main.id 17 | cidr_block = "10.0.1.0/24" 18 | } 19 | output "vpc_id" { 20 | value = aws_vpc.main.id 21 | } 22 | output "subnet_id" { 23 | value = aws_subnet.main.id 24 | } 25 | variable "vpcName" { 26 | description = "VPC name" 27 | type = string 28 | } 29 | vars: 30 | - key: vpcName 31 | value: sample-tf-inline 32 | -------------------------------------------------------------------------------- /examples/cluster/transition/00-mr-tf-workspace/workspace-remote.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: sample-remote 5 | spec: 6 | forProvider: 7 | source: Remote 8 | module: git::https://github.com/ytsarev/provider-terraform-test-module.git//transition?ref=main 9 | vars: 10 | - key: vpcName 11 | value: sample-tf-remote 12 | -------------------------------------------------------------------------------- /examples/cluster/transition/01-composition-tf-only/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: tf-xsubnets.aws.demo.upbound.io 5 | labels: 6 | provider: aws 7 | implementation: terraform 8 | spec: 9 | compositeTypeRef: 10 | apiVersion: aws.demo.upbound.io/v1alpha1 11 | kind: XSubnet 12 | resources: 13 | - name: tf-vpc-and-subnet 14 | base: 15 | apiVersion: tf.upbound.io/v1beta1 16 | kind: Workspace 17 | spec: 18 | forProvider: 19 | source: Inline 20 | module: | 21 | resource "aws_vpc" "main" { 22 | cidr_block = "10.0.0.0/16" 23 | tags = { 24 | Name = var.vpcName 25 | } 26 | } 27 | resource "aws_subnet" "main" { 28 | vpc_id = aws_vpc.main.id 29 | cidr_block = "10.0.1.0/24" 30 | } 31 | output "vpc_id" { 32 | value = aws_vpc.main.id 33 | } 34 | output "subnet_id" { 35 | value = aws_subnet.main.id 36 | } 37 | variable "vpcName" { 38 | description = "VPC name" 39 | type = string 40 | } 41 | vars: 42 | - key: vpcName 43 | patches: 44 | - fromFieldPath: spec.vpcName 45 | toFieldPath: spec.forProvider.vars[0].value 46 | - type: ToCompositeFieldPath 47 | fromFieldPath: status.atProvider.outputs.vpc_id 48 | toFieldPath: status.share.vpcId 49 | policy: 50 | fromFieldPath: Optional 51 | - type: ToCompositeFieldPath 52 | fromFieldPath: status.atProvider.outputs.subnet_id 53 | toFieldPath: status.share.subnetId 54 | policy: 55 | fromFieldPath: Optional 56 | -------------------------------------------------------------------------------- /examples/cluster/transition/01-composition-tf-only/subnet-tf.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: aws.demo.upbound.io/v1alpha1 2 | kind: Subnet 3 | metadata: 4 | name: subnet-tf 5 | spec: 6 | vpcName: provider-terraform-demo-tf 7 | compositionSelector: 8 | matchLabels: 9 | implementation: terraform 10 | -------------------------------------------------------------------------------- /examples/cluster/transition/02-composition-tf-and-native/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: mixed-xsubnets.aws.demo.upbound.io 5 | labels: 6 | provider: aws 7 | implementation: mixed 8 | spec: 9 | compositeTypeRef: 10 | apiVersion: aws.demo.upbound.io/v1alpha1 11 | kind: XSubnet 12 | resources: 13 | - name: vpc 14 | base: 15 | apiVersion: tf.upbound.io/v1beta1 16 | kind: Workspace 17 | spec: 18 | forProvider: 19 | source: Inline 20 | module: | 21 | resource "aws_vpc" "main" { 22 | cidr_block = "10.0.0.0/16" 23 | tags = { 24 | Name = var.vpcName 25 | } 26 | } 27 | output "vpc_id" { 28 | value = aws_vpc.main.id 29 | } 30 | variable "vpcName" { 31 | description = "VPC name" 32 | type = string 33 | } 34 | vars: 35 | - key: vpcName 36 | patches: 37 | - fromFieldPath: spec.vpcName 38 | toFieldPath: spec.forProvider.vars[0].value 39 | - type: ToCompositeFieldPath 40 | fromFieldPath: status.atProvider.outputs.vpc_id 41 | toFieldPath: status.share.vpcId 42 | policy: 43 | fromFieldPath: Optional 44 | - name: subnet 45 | base: 46 | apiVersion: ec2.aws.upbound.io/v1beta1 47 | kind: Subnet 48 | spec: 49 | forProvider: 50 | region: eu-central-1 51 | cidrBlock: 10.0.1.0/24 52 | patches: 53 | - fromFieldPath: status.share.vpcId 54 | toFieldPath: spec.forProvider.vpcId 55 | policy: 56 | fromFieldPath: Required 57 | -------------------------------------------------------------------------------- /examples/cluster/transition/02-composition-tf-and-native/subnet-mixed.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: aws.demo.upbound.io/v1alpha1 2 | kind: Subnet 3 | metadata: 4 | name: subnet-mixed 5 | spec: 6 | vpcName: provider-terraform-demo-mixed 7 | compositionSelector: 8 | matchLabels: 9 | implementation: mixed 10 | -------------------------------------------------------------------------------- /examples/cluster/transition/03-composition-native-only/composition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: Composition 3 | metadata: 4 | name: native-xsubnets.aws.demo.upbound.io 5 | labels: 6 | provider: aws 7 | implementation: native 8 | spec: 9 | compositeTypeRef: 10 | apiVersion: aws.demo.upbound.io/v1alpha1 11 | kind: XSubnet 12 | resources: 13 | - name: vpc 14 | base: 15 | apiVersion: ec2.aws.upbound.io/v1beta1 16 | kind: VPC 17 | spec: 18 | forProvider: 19 | region: eu-central-1 20 | cidrBlock: 10.0.0.0/16 21 | patches: 22 | - fromFieldPath: spec.vpcName 23 | toFieldPath: spec.forProvider.tags.Name 24 | - name: subnet 25 | base: 26 | apiVersion: ec2.aws.upbound.io/v1beta1 27 | kind: Subnet 28 | spec: 29 | forProvider: 30 | region: eu-central-1 31 | cidrBlock: 10.0.1.0/24 32 | vpcIdSelector: 33 | matchControllerRef: true 34 | -------------------------------------------------------------------------------- /examples/cluster/transition/03-composition-native-only/subnet-native.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: aws.demo.upbound.io/v1alpha1 2 | kind: Subnet 3 | metadata: 4 | name: subnet-native 5 | spec: 6 | vpcName: provider-terraform-demo-native 7 | compositionSelector: 8 | matchLabels: 9 | implementation: native 10 | -------------------------------------------------------------------------------- /examples/cluster/transition/README.md: -------------------------------------------------------------------------------- 1 | # provider-terraform demo sample 2 | 3 | This example demonstrates `XSubnet` consistent XRD abstraction with multiple 4 | `Composition` implementations. 5 | 6 | It illustrates the following transition: 7 | 8 | 1. Barebone Managed Resource of [Terraform Workspace](00-mr-tf-workspace/workspace-inline.yaml). 9 | 2. [Terraform Workspace within a Composition](01-composition-tf-only/composition.yaml). 10 | 3. Mixed scenario of [Terraform Workspace and Crossplane-native Resource within the Composition](02-composition-tf-and-native/composition.yaml). 11 | 4. Full migration to [Crossplane-native Managed Resources](03-composition-native-only/composition.yaml). 12 | 13 | All Composition-based steps are backed by the stable custom API defined by [XRD](definition.yaml). 14 | 15 | All Composition-based transition stages can be end-to-end tested by associated 16 | Claim instantiation. 17 | -------------------------------------------------------------------------------- /examples/cluster/transition/definition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.crossplane.io/v1 2 | kind: CompositeResourceDefinition 3 | metadata: 4 | name: xsubnets.aws.demo.upbound.io 5 | spec: 6 | group: aws.demo.upbound.io 7 | names: 8 | kind: XSubnet 9 | plural: xsubnets 10 | claimNames: 11 | kind: Subnet 12 | plural: subnets 13 | versions: 14 | - name: v1alpha1 15 | served: true 16 | referenceable: true 17 | schema: 18 | openAPIV3Schema: 19 | type: object 20 | properties: 21 | spec: 22 | type: object 23 | properties: 24 | vpcName: 25 | type: string 26 | required: 27 | - vpcName 28 | status: 29 | description: A Status represents the observed state 30 | properties: 31 | share: 32 | description: Freeform field containing status information 33 | type: object 34 | x-kubernetes-preserve-unknown-fields: true 35 | type: object 36 | -------------------------------------------------------------------------------- /examples/cluster/workspace-enable-logging.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: example-random-generator 5 | annotations: 6 | meta.upbound.io/example-id: tf/v1beta1/workspace 7 | # The terraform workspace will be named 'random'. If you omit this 8 | # annotation it would be derived from metadata.name - e.g. 'example-random-generator. 9 | crossplane.io/external-name: random 10 | spec: 11 | forProvider: 12 | enableTerraformCLILogging: true 13 | source: Inline 14 | module: | 15 | resource "random_id" "example_id" { 16 | byte_length = 8 17 | } 18 | resource "random_password" "password" { 19 | length = 16 20 | special = true 21 | } 22 | // Non-sensitive Outputs are written to status.atProvider.outputs and to the connection secret. 23 | output "random_id_hex" { 24 | value = random_id.example_id.hex 25 | } 26 | // Sensitive Outputs are only written to the connection secret 27 | output "random_password" { 28 | value = random_password.password 29 | sensitive = true 30 | } 31 | // Terraform has several other random resources, see the random provider for details 32 | writeConnectionSecretToRef: 33 | namespace: default 34 | name: terraform-workspace-example-random-generator 35 | -------------------------------------------------------------------------------- /examples/cluster/workspace-inline-aws.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: sample-inline 5 | spec: 6 | providerConfigRef: 7 | name: aws-eu-west-1 8 | forProvider: 9 | source: Inline 10 | module: | 11 | resource "aws_vpc" "main" { 12 | cidr_block = "10.0.0.0/16" 13 | tags = { 14 | Name = var.vpcName 15 | } 16 | } 17 | resource "aws_subnet" "main" { 18 | vpc_id = aws_vpc.main.id 19 | cidr_block = "10.0.1.0/24" 20 | } 21 | output "vpc_id" { 22 | value = aws_vpc.main.id 23 | } 24 | output "subnet_data" { 25 | value = { 26 | "id" = aws_subnet.main.id 27 | "arn" = aws_subnet.main.arn 28 | } 29 | } 30 | variable "vpcName" { 31 | description = "VPC name" 32 | type = string 33 | } 34 | vars: 35 | - key: vpcName 36 | value: sample-tf-inline 37 | -------------------------------------------------------------------------------- /examples/cluster/workspace-inline.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: example-inline 5 | annotations: 6 | meta.upbound.io/example-id: tf/v1beta1/workspace 7 | # The terraform workspace will be named 'coolbucket'. If you omit this 8 | # annotation it would be derived from metadata.name - e.g. 'example-inline'. 9 | crossplane.io/external-name: coolbucket 10 | spec: 11 | forProvider: 12 | # Workspaces default to using a remote source - like workspace-remote.yaml. 13 | # For simple cases you can use an inline source to specify the content of 14 | # main.tf as opaque, inline HCL. 15 | source: Inline 16 | module: | 17 | // Outputs are written to the connection secret. 18 | output "url" { 19 | value = google_storage_bucket.example.self_link 20 | } 21 | 22 | resource "random_id" "example" { 23 | byte_length = 4 24 | } 25 | 26 | // The google provider and remote state are configured by the provider 27 | // config - see providerconfig.yaml. 28 | resource "google_storage_bucket" "example" { 29 | name = "crossplane-example-${terraform.workspace}-${random_id.example.hex}" 30 | location = "US" 31 | force_destroy = true 32 | 33 | public_access_prevention = "enforced" 34 | } 35 | writeConnectionSecretToRef: 36 | namespace: default 37 | name: terraform-workspace-example-inline 38 | -------------------------------------------------------------------------------- /examples/cluster/workspace-random-generator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: example-random-generator 5 | annotations: 6 | meta.upbound.io/example-id: tf/v1beta1/workspace 7 | # The terraform workspace will be named 'random'. If you omit this 8 | # annotation it would be derived from metadata.name - e.g. 'example-random-generator. 9 | crossplane.io/external-name: random 10 | spec: 11 | forProvider: 12 | source: Inline 13 | module: | 14 | resource "random_id" "example_id" { 15 | byte_length = 4 16 | } 17 | resource "random_password" "password" { 18 | length = 16 19 | special = true 20 | } 21 | // Non-sensitive Outputs are written to status.atProvider.outputs and to the connection secret. 22 | output "random_id_hex" { 23 | value = random_id.example_id.hex 24 | } 25 | // Sensitive Outputs are only written to the connection secret 26 | output "random_password" { 27 | value = random_password.password 28 | sensitive = true 29 | } 30 | // Terraform has several other random resources, see the random provider for details 31 | 32 | writeConnectionSecretToRef: 33 | namespace: default 34 | name: terraform-workspace-example-random-generator 35 | -------------------------------------------------------------------------------- /examples/cluster/workspace-remote.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: example-remote 5 | annotations: 6 | meta.upbound.io/example-id: tf/v1beta1/workspace 7 | # The terraform workspace will be named 'myworkspace'. If you omit this 8 | # annotation it would be derived from metadata.name - e.g. 'example-remote'. 9 | crossplane.io/external-name: myworkspace 10 | spec: 11 | forProvider: 12 | # Git based remote module is supported. 13 | # See https://www.terraform.io/language/modules/sources#generic-git-repository 14 | # and https://www.terraform.io/language/modules/sources#modules-in-package-sub-directories 15 | # for URL structure. 16 | # You can also specify a simple main.tf inline; see 17 | # workspace-inline.yaml. 18 | source: Remote 19 | module: git::https://github.com/crossplane/tf?ref=main 20 | # Variables can be specified inline. 21 | vars: 22 | - key: region 23 | value: us-west-1 24 | # Variable files can be loaded from a ConfigMap or a Secret. 25 | varFiles: 26 | - source: ConfigMapKey 27 | configMapKeyRef: 28 | namespace: default 29 | name: terraform 30 | key: example.tfvars 31 | - source: SecretKey 32 | secretKeyRef: 33 | namespace: default 34 | name: terraform 35 | key: example.tfvar.json 36 | # Variables are expected to be in HCL '.tfvars' format by default. Use 37 | # the JSON format if your variables are in the JSON '.tfvars.json' format. 38 | format: JSON 39 | # All Terraform outputs are written to the connection secret. 40 | writeConnectionSecretToRef: 41 | namespace: default 42 | name: terraform-workspace-example-remote 43 | -------------------------------------------------------------------------------- /examples/namespaced/clusterproviderconfig-aws.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tf.m.upbound.io/v1beta1 3 | kind: ClusterProviderConfig 4 | metadata: 5 | name: aws-eu-west-1-cluster 6 | namespace: upbound-system 7 | spec: 8 | credentials: 9 | - filename: aws-creds.ini 10 | source: Secret 11 | secretRef: 12 | namespace: upbound-system 13 | name: aws-creds 14 | key: credentials 15 | configuration: | 16 | terraform { 17 | required_providers { 18 | aws = { 19 | source = "hashicorp/aws" 20 | version = "5.6.1" 21 | } 22 | } 23 | backend "kubernetes" { 24 | secret_suffix = "providerconfig-aws-eu-west-1-cluster" 25 | namespace = "upbound-system" 26 | in_cluster_config = true 27 | } 28 | } 29 | provider "aws" { 30 | shared_credentials_files = ["${path.module}/aws-creds.ini"] 31 | region = "us-east-2" 32 | } 33 | -------------------------------------------------------------------------------- /examples/namespaced/clusterproviderconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.m.upbound.io/v1beta1 2 | kind: ClusterProviderConfig 3 | metadata: 4 | name: default 5 | namespace: upbound-system 6 | spec: 7 | # Note that unlike most provider configs this one supports an array of 8 | # credentials. This is because each Terraform workspace uses a single 9 | # Crossplane provider config, but could use multiple Terraform providers each 10 | # with their own credentials. 11 | credentials: 12 | - filename: gcp-credentials.json 13 | source: Secret 14 | secretRef: 15 | namespace: upbound-system 16 | name: gcp-creds 17 | key: credentials 18 | # This optional configuration block can be used to inject HCL into any 19 | # workspace that uses this provider config, for example to setup Terraform 20 | # providers. 21 | configuration: | 22 | provider "google" { 23 | credentials = "gcp-credentials.json" 24 | project = "official-provider-testing" 25 | } 26 | 27 | // Modules _must_ use remote state. The provider does not persist state. 28 | terraform { 29 | backend "kubernetes" { 30 | secret_suffix = "clusterproviderconfig-default" 31 | namespace = "upbound-system" 32 | in_cluster_config = true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/namespaced/providerconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.m.upbound.io/v1beta1 2 | kind: ProviderConfig 3 | metadata: 4 | name: default 5 | namespace: upbound-system 6 | spec: 7 | # Note that unlike most provider configs this one supports an array of 8 | # credentials. This is because each Terraform workspace uses a single 9 | # Crossplane provider config, but could use multiple Terraform providers each 10 | # with their own credentials. 11 | credentials: 12 | - filename: gcp-credentials.json 13 | source: Secret 14 | secretRef: 15 | namespace: upbound-system 16 | name: gcp-creds 17 | key: credentials 18 | # This optional configuration block can be used to inject HCL into any 19 | # workspace that uses this provider config, for example to setup Terraform 20 | # providers. 21 | configuration: | 22 | provider "google" { 23 | credentials = "gcp-credentials.json" 24 | project = "official-provider-testing" 25 | } 26 | 27 | // Modules _must_ use remote state. The provider does not persist state. 28 | terraform { 29 | backend "kubernetes" { 30 | secret_suffix = "providerconfig-default" 31 | namespace = "upbound-system" 32 | in_cluster_config = true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/namespaced/workspace-inline-aws.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tf.m.upbound.io/v1beta1 2 | kind: Workspace 3 | metadata: 4 | name: sample-inline 5 | namespace: upbound-system 6 | spec: 7 | providerConfigRef: 8 | name: aws-eu-west-1-cluster 9 | kind: ClusterProviderConfig 10 | forProvider: 11 | source: Inline 12 | module: | 13 | resource "aws_vpc" "main" { 14 | cidr_block = "10.0.0.0/16" 15 | tags = { 16 | Name = var.vpcName 17 | } 18 | } 19 | resource "aws_subnet" "main" { 20 | vpc_id = aws_vpc.main.id 21 | cidr_block = "10.0.1.0/24" 22 | } 23 | output "vpc_id" { 24 | value = aws_vpc.main.id 25 | } 26 | output "subnet_data" { 27 | value = { 28 | "id" = aws_subnet.main.id 29 | "arn" = aws_subnet.main.arn 30 | } 31 | } 32 | variable "vpcName" { 33 | description = "VPC name" 34 | type = string 35 | } 36 | vars: 37 | - key: vpcName 38 | value: sample-tf-inline 39 | -------------------------------------------------------------------------------- /generate/generate.go: -------------------------------------------------------------------------------- 1 | //go:build generate 2 | // +build generate 3 | 4 | /* 5 | Copyright 2020 The Crossplane Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // NOTE: See the below link for details on what is happening here. 21 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 22 | 23 | // Remove existing CRDs 24 | //go:generate rm -rf ../package/crds 25 | 26 | // Generate deepcopy methodsets and CRD manifests 27 | //go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=../apis/... crd:crdVersions=v1 output:artifacts:config=../package/crds 28 | 29 | // Generate crossplane-runtime methodsets (resource.Claim, etc) 30 | //go:generate go run -tags generate github.com/crossplane/crossplane-tools/cmd/angryjet generate-methodsets --header-file=../hack/boilerplate.go.txt ../apis/... 31 | 32 | package generate 33 | 34 | import ( 35 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" //nolint:typecheck 36 | 37 | _ "github.com/crossplane/crossplane-tools/cmd/angryjet" //nolint:typecheck 38 | ) 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/upbound/provider-terraform 2 | 3 | go 1.24.10 4 | 5 | require ( 6 | github.com/MakeNowJust/heredoc v1.0.0 7 | github.com/alecthomas/kingpin/v2 v2.4.0 8 | github.com/crossplane/crossplane-runtime/v2 v2.0.0 9 | github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec 10 | github.com/fluxcd/source-controller/api v1.2.5 11 | github.com/google/go-cmp v0.7.0 12 | github.com/google/uuid v1.6.0 13 | github.com/hashicorp/go-getter v1.7.9 14 | github.com/pkg/errors v0.9.1 15 | github.com/spf13/afero v1.14.0 16 | go.uber.org/zap v1.27.0 17 | google.golang.org/grpc v1.68.1 18 | k8s.io/api v0.33.0 19 | k8s.io/apiextensions-apiserver v0.33.0 20 | k8s.io/apimachinery v0.33.0 21 | k8s.io/client-go v0.33.0 22 | sigs.k8s.io/controller-runtime v0.19.0 23 | sigs.k8s.io/controller-tools v0.18.0 24 | ) 25 | 26 | require ( 27 | cloud.google.com/go v0.112.0 // indirect 28 | cloud.google.com/go/compute/metadata v0.5.0 // indirect 29 | cloud.google.com/go/iam v1.1.5 // indirect 30 | cloud.google.com/go/storage v1.36.0 // indirect 31 | dario.cat/mergo v1.0.1 // indirect 32 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect 33 | github.com/aws/aws-sdk-go v1.44.122 // indirect 34 | github.com/beorn7/perks v1.0.1 // indirect 35 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 36 | github.com/blang/semver/v4 v4.0.0 // indirect 37 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 38 | github.com/dave/jennifer v1.7.1 // indirect 39 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 40 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 41 | github.com/evanphx/json-patch v5.9.11+incompatible // indirect 42 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 43 | github.com/fatih/color v1.18.0 // indirect 44 | github.com/felixge/httpsnoop v1.0.4 // indirect 45 | github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect 46 | github.com/fluxcd/pkg/apis/meta v1.3.0 // indirect 47 | github.com/fsnotify/fsnotify v1.7.0 // indirect 48 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 49 | github.com/go-logr/logr v1.4.2 // indirect 50 | github.com/go-logr/stdr v1.2.2 // indirect 51 | github.com/go-logr/zapr v1.3.0 // indirect 52 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 53 | github.com/go-openapi/jsonreference v0.20.2 // indirect 54 | github.com/go-openapi/swag v0.23.0 // indirect 55 | github.com/gobuffalo/flect v1.0.3 // indirect 56 | github.com/gogo/protobuf v1.3.2 // indirect 57 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 58 | github.com/golang/protobuf v1.5.4 // indirect 59 | github.com/google/gnostic-models v0.6.9 // indirect 60 | github.com/google/s2a-go v0.1.7 // indirect 61 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 62 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 63 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 64 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 65 | github.com/hashicorp/go-version v1.6.0 // indirect 66 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 67 | github.com/jmespath/go-jmespath v0.4.0 // indirect 68 | github.com/josharian/intern v1.0.0 // indirect 69 | github.com/json-iterator/go v1.1.12 // indirect 70 | github.com/klauspost/compress v1.18.0 // indirect 71 | github.com/mailru/easyjson v0.7.7 // indirect 72 | github.com/mattn/go-colorable v0.1.13 // indirect 73 | github.com/mattn/go-isatty v0.0.20 // indirect 74 | github.com/mitchellh/go-homedir v1.1.0 // indirect 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 76 | github.com/modern-go/reflect2 v1.0.2 // indirect 77 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 78 | github.com/prometheus/client_golang v1.22.0 // indirect 79 | github.com/prometheus/client_model v0.6.1 // indirect 80 | github.com/prometheus/common v0.62.0 // indirect 81 | github.com/prometheus/procfs v0.15.1 // indirect 82 | github.com/spf13/cobra v1.9.1 // indirect 83 | github.com/spf13/pflag v1.0.6 // indirect 84 | github.com/ulikunitz/xz v0.5.15 // indirect 85 | github.com/x448/float16 v0.8.4 // indirect 86 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 87 | go.opencensus.io v0.24.0 // indirect 88 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 89 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect 90 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 91 | go.opentelemetry.io/otel v1.33.0 // indirect 92 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 93 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 94 | go.uber.org/multierr v1.11.0 // indirect 95 | golang.org/x/crypto v0.37.0 // indirect 96 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 97 | golang.org/x/mod v0.24.0 // indirect 98 | golang.org/x/net v0.39.0 // indirect 99 | golang.org/x/oauth2 v0.27.0 // indirect 100 | golang.org/x/sync v0.13.0 // indirect 101 | golang.org/x/sys v0.32.0 // indirect 102 | golang.org/x/term v0.31.0 // indirect 103 | golang.org/x/text v0.24.0 // indirect 104 | golang.org/x/time v0.9.0 // indirect 105 | golang.org/x/tools v0.32.0 // indirect 106 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 107 | google.golang.org/api v0.155.0 // indirect 108 | google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect 109 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect 110 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 111 | google.golang.org/protobuf v1.36.5 // indirect 112 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 113 | gopkg.in/inf.v0 v0.9.1 // indirect 114 | gopkg.in/yaml.v2 v2.4.0 // indirect 115 | gopkg.in/yaml.v3 v3.0.1 // indirect 116 | k8s.io/code-generator v0.33.0 // indirect 117 | k8s.io/component-base v0.33.0 // indirect 118 | k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect 119 | k8s.io/klog/v2 v2.130.1 // indirect 120 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 121 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 122 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 123 | sigs.k8s.io/randfill v1.0.0 // indirect 124 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 125 | sigs.k8s.io/yaml v1.4.0 // indirect 126 | ) 127 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /internal/bootcheck/default.go: -------------------------------------------------------------------------------- 1 | //go:build !custombootcheck 2 | // +build !custombootcheck 3 | 4 | package bootcheck 5 | 6 | func CheckEnv() error { 7 | // No-op by default. Use build tags for build-time isolation of custom preflight checks. 8 | // Ensure to update the build tags on L1-L2 so that they are mutually exclusive across implementations. 9 | return nil 10 | } 11 | -------------------------------------------------------------------------------- /internal/clients/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package clients 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | 23 | "github.com/crossplane/crossplane-runtime/v2/pkg/errors" 24 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/types" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | 29 | clusterv1beta1 "github.com/upbound/provider-terraform/apis/cluster/v1beta1" 30 | namespacedv1beta1 "github.com/upbound/provider-terraform/apis/namespaced/v1beta1" 31 | ) 32 | 33 | const ( 34 | errProviderConfigNotSet = "provider config is not set" 35 | errGetProviderConfig = "cannot get provider config" 36 | errFailedToTrackUsage = "cannot track provider config usage" 37 | ) 38 | 39 | func ResolveProviderConfig(ctx context.Context, crClient client.Client, lt LegacyTracker, mt ModernTracker, mg resource.Managed) (*namespacedv1beta1.ClusterProviderConfig, error) { 40 | switch managed := mg.(type) { 41 | case resource.LegacyManaged: 42 | return resolveProviderConfigLegacy(ctx, crClient, managed, lt) 43 | case resource.ModernManaged: 44 | return resolveProviderConfigModern(ctx, crClient, managed, mt) 45 | default: 46 | return nil, errors.New("resource is not a managed") 47 | } 48 | } 49 | 50 | func resolveProviderConfigLegacy(ctx context.Context, client client.Client, mg resource.LegacyManaged, lt LegacyTracker) (*namespacedv1beta1.ClusterProviderConfig, error) { 51 | configRef := mg.GetProviderConfigReference() 52 | if configRef == nil { 53 | return nil, errors.New(errProviderConfigNotSet) 54 | } 55 | pc := &clusterv1beta1.ProviderConfig{} 56 | if err := client.Get(ctx, types.NamespacedName{Name: configRef.Name}, pc); err != nil { 57 | return nil, errors.Wrap(err, errGetProviderConfig) 58 | } 59 | 60 | if err := lt.Track(ctx, mg); err != nil { 61 | return nil, errors.Wrap(err, errFailedToTrackUsage) 62 | } 63 | 64 | return legacyToModernProviderConfigSpec(pc) 65 | } 66 | 67 | func resolveProviderConfigModern(ctx context.Context, crClient client.Client, mg resource.ModernManaged, mt ModernTracker) (*namespacedv1beta1.ClusterProviderConfig, error) { 68 | configRef := mg.GetProviderConfigReference() 69 | if configRef == nil { 70 | return nil, errors.New(errProviderConfigNotSet) 71 | } 72 | 73 | pcRuntimeObj, err := crClient.Scheme().New(namespacedv1beta1.SchemeGroupVersion.WithKind(configRef.Kind)) 74 | if err != nil { 75 | return nil, errors.Wrapf(err, "referenced provider config kind %q is invalid for %s/%s", configRef.Kind, mg.GetNamespace(), mg.GetName()) 76 | } 77 | pcObj, ok := pcRuntimeObj.(resource.ProviderConfig) 78 | if !ok { 79 | return nil, errors.Errorf("referenced provider config kind %q is not a provider config type %s/%s", configRef.Kind, mg.GetNamespace(), mg.GetName()) 80 | } 81 | 82 | // Namespace will be ignored if the PC is a cluster-scoped type 83 | if err := crClient.Get(ctx, types.NamespacedName{Name: configRef.Name, Namespace: mg.GetNamespace()}, pcObj); err != nil { 84 | return nil, errors.Wrap(err, errGetProviderConfig) 85 | } 86 | 87 | var effectivePC *namespacedv1beta1.ClusterProviderConfig 88 | switch pc := pcObj.(type) { 89 | case *namespacedv1beta1.ProviderConfig: 90 | enrichLocalSecretRefs(pc, mg) 91 | effectivePC = &namespacedv1beta1.ClusterProviderConfig{ 92 | TypeMeta: metav1.TypeMeta{ 93 | APIVersion: namespacedv1beta1.SchemeGroupVersion.String(), 94 | Kind: namespacedv1beta1.ClusterProviderConfigKind, 95 | }, 96 | ObjectMeta: metav1.ObjectMeta{ 97 | Name: pc.GetName(), 98 | Labels: pc.GetLabels(), 99 | Annotations: pc.GetAnnotations(), 100 | }, 101 | Spec: pc.Spec, 102 | } 103 | case *namespacedv1beta1.ClusterProviderConfig: 104 | // noop 105 | effectivePC = pc 106 | default: 107 | return nil, errors.New("unknown") 108 | } 109 | 110 | if err := mt.Track(ctx, mg); err != nil { 111 | return nil, errors.Wrap(err, errFailedToTrackUsage) 112 | } 113 | return effectivePC, nil 114 | } 115 | 116 | func legacyToModernProviderConfigSpec(pc *clusterv1beta1.ProviderConfig) (*namespacedv1beta1.ClusterProviderConfig, error) { 117 | // TODO(erhan): this is hacky and potentially lossy, generate or manually implement 118 | if pc == nil { 119 | return nil, nil 120 | } 121 | data, err := json.Marshal(pc) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | var mSpec namespacedv1beta1.ClusterProviderConfig 127 | err = json.Unmarshal(data, &mSpec) 128 | mSpec.TypeMeta.Kind = namespacedv1beta1.ClusterProviderConfigKind 129 | mSpec.TypeMeta.APIVersion = namespacedv1beta1.SchemeGroupVersion.String() 130 | mSpec.ObjectMeta = metav1.ObjectMeta{ 131 | Name: pc.GetName(), 132 | Labels: pc.GetLabels(), 133 | Annotations: pc.GetAnnotations(), 134 | } 135 | return &mSpec, err 136 | } 137 | 138 | func enrichLocalSecretRefs(pc *namespacedv1beta1.ProviderConfig, mg resource.Managed) { 139 | if pc != nil { 140 | if pc.Spec.Credentials != nil { 141 | for _, v := range pc.Spec.Credentials { 142 | if v.SecretRef != nil { 143 | v.SecretRef.Namespace = mg.GetNamespace() 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /internal/clients/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package clients 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 23 | ) 24 | 25 | // TODO: Remove these temporary interfaces once crossplane-runtime supports them 26 | // natively. These interfaces enable unit testing by providing mockable Track 27 | // functions for both legacy and modern managed resources. They are temporary 28 | // workarounds pending the merge of https://github.com/crossplane/crossplane-runtime/pull/862, 29 | // after which we should migrate to the upstream implementations. 30 | 31 | // A LegacyTracker tracks legacy managed resources. 32 | type LegacyTracker interface { 33 | // Track the supplied legacy managed resource. 34 | Track(ctx context.Context, mg resource.LegacyManaged) error 35 | } 36 | 37 | // A LegacyTrackerFn is a function that tracks managed resources. 38 | type LegacyTrackerFn func(ctx context.Context, mg resource.LegacyManaged) error 39 | 40 | // Track the supplied legacy managed resource. 41 | func (fn LegacyTrackerFn) Track(ctx context.Context, mg resource.LegacyManaged) error { 42 | return fn(ctx, mg) 43 | } 44 | 45 | // A ModernTracker tracks modern managed resources. 46 | type ModernTracker interface { 47 | // Track the supplied modern managed resource. 48 | Track(ctx context.Context, mg resource.ModernManaged) error 49 | } 50 | 51 | // A ModernTrackerFn is a function that tracks managed resources. 52 | type ModernTrackerFn func(ctx context.Context, mg resource.ModernManaged) error 53 | 54 | // Track the supplied managed resource. 55 | func (fn ModernTrackerFn) Track(ctx context.Context, mg resource.ModernManaged) error { 56 | return fn(ctx, mg) 57 | } 58 | -------------------------------------------------------------------------------- /internal/controller/cluster/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "time" 21 | 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | 24 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 25 | "github.com/crossplane/crossplane-runtime/v2/pkg/event" 26 | "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" 27 | "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/providerconfig" 28 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 29 | 30 | "github.com/upbound/provider-terraform/apis/cluster/v1beta1" 31 | ) 32 | 33 | // Setup adds a controller that reconciles ProviderConfigs by accounting for 34 | // their current usage. 35 | func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 36 | name := providerconfig.ControllerName(v1beta1.ProviderConfigGroupKind) 37 | 38 | of := resource.ProviderConfigKinds{ 39 | Config: v1beta1.ProviderConfigGroupVersionKind, 40 | Usage: v1beta1.ProviderConfigUsageGroupVersionKind, 41 | UsageList: v1beta1.ProviderConfigUsageListGroupVersionKind, 42 | } 43 | 44 | r := providerconfig.NewReconciler(mgr, of, 45 | providerconfig.WithLogger(o.Logger.WithValues("controller", name)), 46 | providerconfig.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) 47 | 48 | return ctrl.NewControllerManagedBy(mgr). 49 | Named(name). 50 | WithOptions(o.ForControllerRuntime()). 51 | For(&v1beta1.ProviderConfig{}). 52 | Watches(&v1beta1.ProviderConfigUsage{}, &resource.EnqueueRequestForProviderConfig{}). 53 | Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) 54 | } 55 | 56 | // SetupGated adds a controller that reconciles ProviderConfigs by accounting for 57 | // their current usage. 58 | func SetupGated(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 59 | o.Gate.Register(func() { 60 | if err := Setup(mgr, o, timeout, pollJitter); err != nil { 61 | mgr.GetLogger().Error(err, "unable to setup reconciler", "gvk", v1beta1.ProviderConfigGroupVersionKind.String()) 62 | } 63 | }, v1beta1.ProviderConfigGroupVersionKind, v1beta1.ProviderConfigUsageGroupVersionKind) 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/controller/cluster/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cluster 18 | -------------------------------------------------------------------------------- /internal/controller/cluster/terraform.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cluster 18 | 19 | import ( 20 | "time" 21 | 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | 24 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 25 | 26 | "github.com/upbound/provider-terraform/internal/controller/cluster/config" 27 | "github.com/upbound/provider-terraform/internal/controller/cluster/workspace" 28 | ) 29 | 30 | // Setup creates all TF controllers with the supplied logger and adds them 31 | // to the supplied manager. 32 | func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 33 | for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration, time.Duration) error{ 34 | config.Setup, 35 | workspace.Setup, 36 | } { 37 | if err := setup(mgr, o, timeout, pollJitter); err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | // SetupGated creates all controllers with the supplied logger and adds them to 45 | // the supplied manager gated. 46 | func SetupGated(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 47 | for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration, time.Duration) error{ 48 | config.SetupGated, 49 | workspace.SetupGated, 50 | } { 51 | if err := setup(mgr, o, timeout, pollJitter); err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/controller/namespaced/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "time" 21 | 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | 24 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 25 | "github.com/crossplane/crossplane-runtime/v2/pkg/event" 26 | "github.com/crossplane/crossplane-runtime/v2/pkg/ratelimiter" 27 | "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/providerconfig" 28 | "github.com/crossplane/crossplane-runtime/v2/pkg/resource" 29 | 30 | "github.com/upbound/provider-terraform/apis/namespaced/v1beta1" 31 | ) 32 | 33 | // Setup adds a controller that reconciles ProviderConfigs by accounting for 34 | // their current usage. 35 | func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 36 | name := providerconfig.ControllerName(v1beta1.ProviderConfigGroupKind) 37 | 38 | of := resource.ProviderConfigKinds{ 39 | Config: v1beta1.ProviderConfigGroupVersionKind, 40 | Usage: v1beta1.ProviderConfigUsageGroupVersionKind, 41 | UsageList: v1beta1.ProviderConfigUsageListGroupVersionKind, 42 | } 43 | 44 | r := providerconfig.NewReconciler(mgr, of, 45 | providerconfig.WithLogger(o.Logger.WithValues("controller", name)), 46 | providerconfig.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) 47 | 48 | return ctrl.NewControllerManagedBy(mgr). 49 | Named(name). 50 | WithOptions(o.ForControllerRuntime()). 51 | For(&v1beta1.ProviderConfig{}). 52 | Watches(&v1beta1.ProviderConfigUsage{}, &resource.EnqueueRequestForProviderConfig{}). 53 | Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) 54 | } 55 | 56 | // SetupGated adds a controller that reconciles ProviderConfigs by accounting for 57 | // their current usage. 58 | func SetupGated(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 59 | o.Gate.Register(func() { 60 | if err := Setup(mgr, o, timeout, pollJitter); err != nil { 61 | mgr.GetLogger().Error(err, "unable to setup reconciler", "gvk", v1beta1.ProviderConfigGroupVersionKind.String()) 62 | } 63 | }, v1beta1.ProviderConfigGroupVersionKind, v1beta1.ProviderConfigUsageGroupVersionKind) 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/controller/namespaced/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package namespaced 18 | -------------------------------------------------------------------------------- /internal/controller/namespaced/terraform.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package namespaced 18 | 19 | import ( 20 | "time" 21 | 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | 24 | "github.com/crossplane/crossplane-runtime/v2/pkg/controller" 25 | 26 | "github.com/upbound/provider-terraform/internal/controller/namespaced/config" 27 | "github.com/upbound/provider-terraform/internal/controller/namespaced/workspace" 28 | ) 29 | 30 | // Setup creates all TF controllers with the supplied logger and adds them 31 | // to the supplied manager. 32 | func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 33 | for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration, time.Duration) error{ 34 | config.Setup, 35 | workspace.Setup, 36 | } { 37 | if err := setup(mgr, o, timeout, pollJitter); err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | // SetupGated creates all controllers with the supplied logger and adds them to 45 | // the supplied manager gated. 46 | func SetupGated(mgr ctrl.Manager, o controller.Options, timeout time.Duration, pollJitter time.Duration) error { 47 | for _, setup := range []func(ctrl.Manager, controller.Options, time.Duration, time.Duration) error{ 48 | config.SetupGated, 49 | workspace.SetupGated, 50 | } { 51 | if err := setup(mgr, o, timeout, pollJitter); err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/features/features.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package features 18 | 19 | import "github.com/crossplane/crossplane-runtime/v2/pkg/feature" 20 | 21 | // Feature flags. 22 | const ( 23 | // EnableAlphaExternalSecretStores enables alpha support for 24 | // External Secret Stores. See the below design for more details. 25 | // https://github.com/crossplane/crossplane/blob/390ddd/design/design-doc-external-secret-stores.md 26 | EnableAlphaExternalSecretStores feature.Flag = "EnableAlphaExternalSecretStores" 27 | 28 | // EnableBetaManagementPolicies enables beta support for 29 | // Management Policies. See the below design for more details. 30 | // https://github.com/crossplane/crossplane/pull/3531 31 | EnableBetaManagementPolicies feature.Flag = "EnableBetaManagementPolicies" 32 | ) 33 | -------------------------------------------------------------------------------- /internal/terraform/testdata/invalidmodule/main.tf: -------------------------------------------------------------------------------- 1 | variable "coolness" { 2 | default = "very" 3 | description = "How cool is this test?" 4 | } 5 | 6 | // This output block is missing a trailing bracket. 7 | output "coolness" { 8 | value = var.coolness 9 | description = "The coolness of this test." 10 | -------------------------------------------------------------------------------- /internal/terraform/testdata/nullmodule/main.tf: -------------------------------------------------------------------------------- 1 | variable "coolness" { 2 | default = "very" 3 | description = "How cool is this test?" 4 | } 5 | 6 | output "coolness" { 7 | value = var.coolness 8 | description = "The coolness of this test." 9 | } 10 | 11 | output "randomness" { 12 | value = random_id.test.hex 13 | description = "A random string." 14 | } 15 | 16 | terraform { 17 | required_providers { 18 | null = { 19 | version = "3.2.4" 20 | } 21 | random = { 22 | version = "3.7.2" 23 | } 24 | } 25 | } 26 | 27 | resource "random_id" "test" { 28 | byte_length = 4 29 | } 30 | 31 | resource "null_resource" "test" { 32 | triggers = { 33 | trigger = random_id.test.hex 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/terraform/testdata/nullmodule/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.14.7", 4 | "serial": 3, 5 | "lineage": "7b9c5b6b-ce2c-9cb9-f940-b696b4454038", 6 | "outputs": { 7 | "coolness": { 8 | "value": "very", 9 | "type": "string" 10 | }, 11 | "randomness": { 12 | "value": "f33da529", 13 | "type": "string" 14 | } 15 | }, 16 | "resources": [ 17 | { 18 | "mode": "managed", 19 | "type": "null_resource", 20 | "name": "test", 21 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 22 | "instances": [ 23 | { 24 | "schema_version": 0, 25 | "attributes": { 26 | "id": "1790215571976780205", 27 | "triggers": { 28 | "trigger": "f33da529" 29 | } 30 | }, 31 | "sensitive_attributes": [], 32 | "private": "bnVsbA==", 33 | "dependencies": [ 34 | "random_id.test" 35 | ] 36 | } 37 | ] 38 | }, 39 | { 40 | "mode": "managed", 41 | "type": "random_id", 42 | "name": "test", 43 | "provider": "provider[\"registry.terraform.io/hashicorp/random\"]", 44 | "instances": [ 45 | { 46 | "schema_version": 0, 47 | "attributes": { 48 | "b64_std": "8z2lKQ==", 49 | "b64_url": "8z2lKQ", 50 | "byte_length": 4, 51 | "dec": "4080903465", 52 | "hex": "f33da529", 53 | "id": "8z2lKQ", 54 | "keepers": null, 55 | "prefix": null 56 | }, 57 | "sensitive_attributes": [], 58 | "private": "bnVsbA==" 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /internal/terraform/testdata/outputmodule/main.tf: -------------------------------------------------------------------------------- 1 | output "string" { 2 | value = "very" 3 | } 4 | 5 | output "sensitive" { 6 | value = "very" 7 | sensitive = true 8 | } 9 | 10 | output "tuple" { 11 | value = ["a", "really", "long", "tuple"] 12 | } 13 | 14 | output "object" { 15 | value = { 16 | wow : "suchobject" 17 | } 18 | } 19 | 20 | output "bool" { 21 | value = true 22 | } 23 | 24 | output "number" { 25 | value = 42 26 | } 27 | -------------------------------------------------------------------------------- /internal/terraform/testdata/outputmodule/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.14.7", 4 | "serial": 4, 5 | "lineage": "cabe0872-bb95-a2aa-53c4-7d034376c1f0", 6 | "outputs": { 7 | "bool": { 8 | "value": true, 9 | "type": "bool" 10 | }, 11 | "number": { 12 | "value": 42, 13 | "type": "number" 14 | }, 15 | "object": { 16 | "value": { 17 | "wow": "suchobject" 18 | }, 19 | "type": [ 20 | "object", 21 | { 22 | "wow": "string" 23 | } 24 | ] 25 | }, 26 | "sensitive": { 27 | "value": "very", 28 | "type": "string", 29 | "sensitive": true 30 | }, 31 | "string": { 32 | "value": "very", 33 | "type": "string" 34 | }, 35 | "tuple": { 36 | "value": [ 37 | "a", 38 | "really", 39 | "long", 40 | "tuple" 41 | ], 42 | "type": [ 43 | "tuple", 44 | [ 45 | "string", 46 | "string", 47 | "string", 48 | "string" 49 | ] 50 | ] 51 | } 52 | }, 53 | "resources": [] 54 | } 55 | -------------------------------------------------------------------------------- /internal/terraform/testdata/validmodule/main.tf: -------------------------------------------------------------------------------- 1 | variable "coolness" { 2 | default = "very" 3 | description = "How cool is this test?" 4 | } 5 | 6 | output "coolness" { 7 | value = var.coolness 8 | description = "The coolness of this test." 9 | } 10 | -------------------------------------------------------------------------------- /internal/workdir/workdir.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workdir 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "strings" 23 | "time" 24 | 25 | "github.com/crossplane/crossplane-runtime/v2/pkg/logging" 26 | "github.com/google/uuid" 27 | "github.com/pkg/errors" 28 | "github.com/spf13/afero" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | 31 | clusterv1beta1 "github.com/upbound/provider-terraform/apis/cluster/v1beta1" 32 | namespacedv1beta1 "github.com/upbound/provider-terraform/apis/namespaced/v1beta1" 33 | ) 34 | 35 | // Error strings. 36 | const ( 37 | errListWorkspaces = "cannot list workspaces" 38 | errFmtReadDir = "cannot read directory %q" 39 | ) 40 | 41 | // A GarbageCollector garbage collects the working directories of Terraform 42 | // workspaces that no longer exist. 43 | type GarbageCollector struct { 44 | kube client.Client 45 | parentDir string 46 | fs afero.Afero 47 | interval time.Duration 48 | log logging.Logger 49 | } 50 | 51 | // A GarbageCollectorOption configures a new GarbageCollector. 52 | type GarbageCollectorOption func(*GarbageCollector) 53 | 54 | // WithFs configures the afero filesystem implementation in which work dirs will 55 | // be garbage collected. The default is the real operating system filesystem. 56 | func WithFs(fs afero.Afero) GarbageCollectorOption { 57 | return func(gc *GarbageCollector) { gc.fs = fs } 58 | } 59 | 60 | // WithInterval configures how often garbage collection will run. The default 61 | // interval is one hour. 62 | func WithInterval(i time.Duration) GarbageCollectorOption { 63 | return func(gc *GarbageCollector) { gc.interval = i } 64 | } 65 | 66 | // WithLogger configures the logger that will be used. The default is a no-op 67 | // logger never emits logs. 68 | func WithLogger(l logging.Logger) GarbageCollectorOption { 69 | return func(gc *GarbageCollector) { gc.log = l } 70 | } 71 | 72 | // NewGarbageCollector returns a garbage collector that garbage collects the 73 | // working directories of Terraform workspaces. 74 | func NewGarbageCollector(c client.Client, parentDir string, o ...GarbageCollectorOption) *GarbageCollector { 75 | gc := &GarbageCollector{ 76 | kube: c, 77 | parentDir: parentDir, 78 | fs: afero.Afero{Fs: afero.NewOsFs()}, 79 | interval: 1 * time.Hour, 80 | log: logging.NewNopLogger(), 81 | } 82 | 83 | for _, fn := range o { 84 | fn(gc) 85 | } 86 | 87 | return gc 88 | } 89 | 90 | // Run the garbage collector. Blocks until the supplied context is done. 91 | func (gc *GarbageCollector) Run(ctx context.Context, namespaced bool) { 92 | t := time.NewTicker(gc.interval) 93 | for { 94 | select { 95 | case <-ctx.Done(): 96 | return 97 | case <-t.C: 98 | if err := gc.collect(ctx, namespaced); err != nil { 99 | gc.log.Info("Garbage collection failed", "error", err) 100 | } 101 | } 102 | } 103 | } 104 | 105 | func isUUID(u string) bool { 106 | _, err := uuid.Parse(u) 107 | return err == nil 108 | } 109 | 110 | func (gc *GarbageCollector) collect(ctx context.Context, namespaced bool) error { 111 | exists := map[string]bool{} 112 | 113 | if namespaced { 114 | l := &namespacedv1beta1.WorkspaceList{} 115 | if err := gc.kube.List(ctx, l); err != nil { 116 | return errors.Wrap(err, errListWorkspaces) 117 | } 118 | for _, ws := range l.Items { 119 | exists[string(ws.GetUID())] = true 120 | } 121 | } else { 122 | l := &clusterv1beta1.WorkspaceList{} 123 | if err := gc.kube.List(ctx, l); err != nil { 124 | return errors.Wrap(err, errListWorkspaces) 125 | } 126 | for _, ws := range l.Items { 127 | exists[string(ws.GetUID())] = true 128 | } 129 | } 130 | fis, err := gc.fs.ReadDir(gc.parentDir) 131 | if err != nil { 132 | return errors.Wrapf(err, errFmtReadDir, gc.parentDir) 133 | } 134 | 135 | failed := make([]string, 0) 136 | for _, fi := range fis { 137 | if !fi.IsDir() || !isUUID(fi.Name()) { 138 | continue 139 | } 140 | if exists[fi.Name()] { 141 | continue 142 | } 143 | path := filepath.Join(gc.parentDir, fi.Name()) 144 | if err := gc.fs.RemoveAll(path); err != nil { 145 | failed = append(failed, path) 146 | } 147 | } 148 | 149 | if len(failed) > 0 { 150 | return errors.Errorf("could not delete directories: %v", strings.Join(failed, ", ")) 151 | } 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /internal/workdir/workdir_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workdir 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/google/go-cmp/cmp" 26 | "github.com/google/go-cmp/cmp/cmpopts" 27 | "github.com/pkg/errors" 28 | "github.com/spf13/afero" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/apimachinery/pkg/types" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | 33 | "github.com/crossplane/crossplane-runtime/v2/pkg/test" 34 | 35 | "github.com/upbound/provider-terraform/apis/cluster/v1beta1" 36 | ) 37 | 38 | func withDirs(fs afero.Afero, dir ...string) afero.Afero { 39 | for _, d := range dir { 40 | _ = fs.Mkdir(d, os.ModePerm) 41 | } 42 | return fs 43 | } 44 | 45 | func getDirs(fs afero.Afero, parentDir string) []string { 46 | dirs := make([]string, 0) 47 | fis, _ := fs.ReadDir(parentDir) 48 | for _, fi := range fis { 49 | if !fi.IsDir() { 50 | continue 51 | } 52 | dirs = append(dirs, fi.Name()) 53 | } 54 | return dirs 55 | } 56 | 57 | func TestCollect(t *testing.T) { 58 | parentDir := "/test" 59 | 60 | type fields struct { 61 | kube client.Client 62 | parentdDir string 63 | fs afero.Afero 64 | } 65 | type args struct { 66 | ctx context.Context 67 | } 68 | type want struct { 69 | dirs []string 70 | err error 71 | } 72 | cases := map[string]struct { 73 | reason string 74 | fields fields 75 | args args 76 | want want 77 | }{ 78 | "ErrNoParentDir": { 79 | reason: "Garbage collection should fail when the parent directory does not exist.", 80 | fields: fields{ 81 | kube: &test.MockClient{MockList: test.NewMockListFn(nil)}, 82 | parentdDir: parentDir, 83 | fs: afero.Afero{Fs: afero.NewMemMapFs()}, 84 | }, 85 | want: want{ 86 | err: errors.Wrapf(errors.Errorf("open %s: file does not exist", parentDir), errFmtReadDir, parentDir), 87 | }, 88 | }, 89 | "NoOp": { 90 | reason: "Garbage collection should succeed when there are no workspaces or workdirs.", 91 | fields: fields{ 92 | kube: &test.MockClient{MockList: test.NewMockListFn(nil)}, 93 | parentdDir: parentDir, 94 | fs: withDirs(afero.Afero{Fs: afero.NewMemMapFs()}, parentDir), 95 | }, 96 | want: want{ 97 | err: nil, 98 | }, 99 | }, 100 | "Success": { 101 | reason: "Workdirs belonging to workspaces that no longer exist should be successfully garbage collected.", 102 | fields: fields{ 103 | kube: &test.MockClient{MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error { 104 | *obj.(*v1beta1.WorkspaceList) = v1beta1.WorkspaceList{Items: []v1beta1.Workspace{ 105 | {ObjectMeta: metav1.ObjectMeta{UID: types.UID("8371dd9e-dd3f-4a42-bd8c-340c4744f6de")}}, 106 | {ObjectMeta: metav1.ObjectMeta{UID: types.UID("ebaac629-43a3-4b39-8138-d7ac19cafe11")}}, 107 | }} 108 | return nil 109 | })}, 110 | parentdDir: parentDir, 111 | fs: withDirs(afero.Afero{Fs: afero.NewMemMapFs()}, 112 | parentDir, 113 | filepath.Join(parentDir, "8371dd9e-dd3f-4a42-bd8c-340c4744f6de"), 114 | filepath.Join(parentDir, "ebaac629-43a3-4b39-8138-d7ac19cafe11"), 115 | filepath.Join(parentDir, "0d177133-1a2f-4ce2-93d2-f8212d3344e7"), 116 | filepath.Join(parentDir, "helm"), 117 | filepath.Join(parentDir, "registry.terraform.io"), 118 | ), 119 | }, 120 | want: want{ 121 | dirs: []string{"8371dd9e-dd3f-4a42-bd8c-340c4744f6de", "ebaac629-43a3-4b39-8138-d7ac19cafe11", "helm", "registry.terraform.io"}, 122 | }, 123 | }, 124 | } 125 | 126 | for name, tc := range cases { 127 | t.Run(name, func(t *testing.T) { 128 | gc := NewGarbageCollector(tc.fields.kube, tc.fields.parentdDir, WithFs(tc.fields.fs)) 129 | err := gc.collect(tc.args.ctx, false) 130 | if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 131 | t.Errorf("gc.collect(...): -want error, +got error:\n%s", diff) 132 | } 133 | 134 | got := getDirs(tc.fields.fs, tc.fields.parentdDir) 135 | if diff := cmp.Diff(tc.want.dirs, got, cmpopts.EquateEmpty()); diff != "" { 136 | t.Errorf("gc.collect(...): -want dirs, +got dirs:\n%s", diff) 137 | } 138 | }) 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /package/crds/tf.m.upbound.io_providerconfigusages.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.18.0 7 | name: providerconfigusages.tf.m.upbound.io 8 | spec: 9 | group: tf.m.upbound.io 10 | names: 11 | categories: 12 | - crossplane 13 | - provider 14 | - terraform 15 | kind: ProviderConfigUsage 16 | listKind: ProviderConfigUsageList 17 | plural: providerconfigusages 18 | singular: providerconfigusage 19 | scope: Namespaced 20 | versions: 21 | - additionalPrinterColumns: 22 | - jsonPath: .metadata.creationTimestamp 23 | name: AGE 24 | type: date 25 | - jsonPath: .providerConfigRef.name 26 | name: CONFIG-NAME 27 | type: string 28 | - jsonPath: .resourceRef.kind 29 | name: RESOURCE-KIND 30 | type: string 31 | - jsonPath: .resourceRef.name 32 | name: RESOURCE-NAME 33 | type: string 34 | name: v1beta1 35 | schema: 36 | openAPIV3Schema: 37 | description: A ProviderConfigUsage indicates that a resource is using a ProviderConfig. 38 | properties: 39 | apiVersion: 40 | description: |- 41 | APIVersion defines the versioned schema of this representation of an object. 42 | Servers should convert recognized schemas to the latest internal value, and 43 | may reject unrecognized values. 44 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 45 | type: string 46 | kind: 47 | description: |- 48 | Kind is a string value representing the REST resource this object represents. 49 | Servers may infer this from the endpoint the client submits requests to. 50 | Cannot be updated. 51 | In CamelCase. 52 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 53 | type: string 54 | metadata: 55 | type: object 56 | providerConfigRef: 57 | description: ProviderConfigReference to the provider config being used. 58 | properties: 59 | kind: 60 | description: Kind of the referenced object. 61 | type: string 62 | name: 63 | description: Name of the referenced object. 64 | type: string 65 | required: 66 | - kind 67 | - name 68 | type: object 69 | resourceRef: 70 | description: ResourceReference to the managed resource using the provider 71 | config. 72 | properties: 73 | apiVersion: 74 | description: APIVersion of the referenced object. 75 | type: string 76 | kind: 77 | description: Kind of the referenced object. 78 | type: string 79 | name: 80 | description: Name of the referenced object. 81 | type: string 82 | uid: 83 | description: UID of the referenced object. 84 | type: string 85 | required: 86 | - apiVersion 87 | - kind 88 | - name 89 | type: object 90 | required: 91 | - providerConfigRef 92 | - resourceRef 93 | type: object 94 | served: true 95 | storage: true 96 | subresources: {} 97 | -------------------------------------------------------------------------------- /package/crds/tf.upbound.io_providerconfigusages.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.18.0 7 | name: providerconfigusages.tf.upbound.io 8 | spec: 9 | group: tf.upbound.io 10 | names: 11 | categories: 12 | - crossplane 13 | - provider 14 | - terraform 15 | kind: ProviderConfigUsage 16 | listKind: ProviderConfigUsageList 17 | plural: providerconfigusages 18 | singular: providerconfigusage 19 | scope: Cluster 20 | versions: 21 | - additionalPrinterColumns: 22 | - jsonPath: .metadata.creationTimestamp 23 | name: AGE 24 | type: date 25 | - jsonPath: .providerConfigRef.name 26 | name: CONFIG-NAME 27 | type: string 28 | - jsonPath: .resourceRef.kind 29 | name: RESOURCE-KIND 30 | type: string 31 | - jsonPath: .resourceRef.name 32 | name: RESOURCE-NAME 33 | type: string 34 | name: v1beta1 35 | schema: 36 | openAPIV3Schema: 37 | description: A ProviderConfigUsage indicates that a resource is using a ProviderConfig. 38 | properties: 39 | apiVersion: 40 | description: |- 41 | APIVersion defines the versioned schema of this representation of an object. 42 | Servers should convert recognized schemas to the latest internal value, and 43 | may reject unrecognized values. 44 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 45 | type: string 46 | kind: 47 | description: |- 48 | Kind is a string value representing the REST resource this object represents. 49 | Servers may infer this from the endpoint the client submits requests to. 50 | Cannot be updated. 51 | In CamelCase. 52 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 53 | type: string 54 | metadata: 55 | type: object 56 | providerConfigRef: 57 | description: ProviderConfigReference to the provider config being used. 58 | properties: 59 | name: 60 | description: Name of the referenced object. 61 | type: string 62 | policy: 63 | description: Policies for referencing. 64 | properties: 65 | resolution: 66 | default: Required 67 | description: |- 68 | Resolution specifies whether resolution of this reference is required. 69 | The default is 'Required', which means the reconcile will fail if the 70 | reference cannot be resolved. 'Optional' means this reference will be 71 | a no-op if it cannot be resolved. 72 | enum: 73 | - Required 74 | - Optional 75 | type: string 76 | resolve: 77 | description: |- 78 | Resolve specifies when this reference should be resolved. The default 79 | is 'IfNotPresent', which will attempt to resolve the reference only when 80 | the corresponding field is not present. Use 'Always' to resolve the 81 | reference on every reconcile. 82 | enum: 83 | - Always 84 | - IfNotPresent 85 | type: string 86 | type: object 87 | required: 88 | - name 89 | type: object 90 | resourceRef: 91 | description: ResourceReference to the managed resource using the provider 92 | config. 93 | properties: 94 | apiVersion: 95 | description: APIVersion of the referenced object. 96 | type: string 97 | kind: 98 | description: Kind of the referenced object. 99 | type: string 100 | name: 101 | description: Name of the referenced object. 102 | type: string 103 | uid: 104 | description: UID of the referenced object. 105 | type: string 106 | required: 107 | - apiVersion 108 | - kind 109 | - name 110 | type: object 111 | required: 112 | - providerConfigRef 113 | - resourceRef 114 | type: object 115 | served: true 116 | storage: true 117 | subresources: {} 118 | -------------------------------------------------------------------------------- /package/crossplane.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: meta.pkg.crossplane.io/v1 2 | kind: Provider 3 | metadata: 4 | name: provider-terraform 5 | annotations: 6 | meta.crossplane.io/maintainer: Upbound 7 | meta.crossplane.io/source: github.com/upbound/provider-terraform 8 | meta.crossplane.io/description: | 9 | Crossplane provider to manage Terraform workspaces. 10 | meta.crossplane.io/readme: | 11 | Provider Terraform is a Crossplane provider that lets you manage resources 12 | using virtual Terraform workspaces. It is developed and supported by 13 | Upbound. Available resources and their fields can be found in the [Upbound 14 | Marketplace](https://marketplace.upbound.io/providers/upbound/provider-terraform). 15 | friendly-name.meta.crossplane.io: Provider Terraform 16 | spec: 17 | capabilities: 18 | - SafeStart 19 | -------------------------------------------------------------------------------- /scripts/check-examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import yaml 4 | import os 5 | import sys 6 | 7 | 8 | def load_gvks(path, loader): 9 | types = set() 10 | for root, _, files in os.walk(path): 11 | for f in files: 12 | if f.endswith(".yaml"): 13 | with open(os.path.join(root, f)) as s: 14 | for t in yaml.safe_load_all(s): 15 | for gvk in loader(t): 16 | types.add(gvk) 17 | return types 18 | 19 | 20 | def load_crd_type(t): 21 | kind = t["spec"]["names"]["kind"] 22 | group = t["spec"]["group"] 23 | for v in t["spec"]["versions"]: 24 | yield f'{kind}.{group}/{v["name"]}' 25 | 26 | 27 | exception_set = { 28 | 'ProviderConfigUsage.tf.upbound.io/v1beta1', 29 | 'StoreConfig.tf.upbound.io/v1beta1', 30 | 'ProviderConfigUsage.tf.m.upbound.io/v1beta1' 31 | } 32 | 33 | 34 | # Example usage: check-examples.py 35 | if __name__ == "__main__": 36 | if len(sys.argv) != 3: 37 | print("Example usage: check-examples.py ") 38 | sys.exit(1) 39 | known_crd_types = load_gvks(sys.argv[1], load_crd_type) 40 | example_types = load_gvks(sys.argv[2], lambda t: [] if t is None or not {"kind", "apiVersion"}.issubset(t.keys()) 41 | else [f'{t["kind"]}.{t["apiVersion"]}']) 42 | diff = known_crd_types.difference(example_types.union(exception_set)) 43 | if len(diff) == 0: 44 | print("All CRDs have at least one example...") 45 | print(f'Exceptions allowed for: {exception_set}') 46 | sys.exit(0) 47 | print(f'Please add example manifests for the following types: {diff}') 48 | sys.exit(2) 49 | --------------------------------------------------------------------------------