├── .changes ├── 2.2.0.md ├── 2.3.0.md ├── 2.4.0.md ├── 2.4.1.md ├── 2.5.0.md ├── 2.6.0.md ├── 2.6.1.md ├── 2.7.0.md ├── 2.7.1.md ├── 2.8.0.md ├── 2.8.1.md ├── 2.9.0.md ├── 2.9.1.md ├── 2.9.2.md └── unreleased │ └── .gitkeep ├── .changie.yaml ├── .copywrite.hcl ├── .dockerignore ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ └── question.md ├── SECURITY.md ├── dependabot.yml ├── hcp-terraform-logo.png ├── pr-labeler.yml ├── pull_request_template.md ├── test-infra │ └── tfe │ │ ├── README.md │ │ ├── initial-admin │ │ └── admin.tf │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── terraform.auto.tfvars │ │ ├── variables.tf │ │ └── versions.tf └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── dependabot-pr.yaml │ ├── docker-image-security-scan.yml │ ├── end-to-end-tfc.yaml │ ├── end-to-end-tfe.yaml │ ├── hc-copywrite.yml │ ├── helm-chart-unit.yaml │ ├── helm-docs.yml │ ├── helm-end-to-end-tfc.yaml │ ├── helm-end-to-end-tfe.yaml │ ├── jira-issues.yaml │ ├── jira-pr.yaml │ ├── markdown-link-check.yaml │ ├── pr-labeler.yml │ ├── tag-release.yaml │ └── unit-tests.yaml ├── .gitignore ├── .release ├── ci.hcl ├── release-metadata.hcl ├── security-scan.hcl └── terraform-cloud-operator-artifacts.hcl ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── META.d ├── _summary.yaml └── links.yaml ├── Makefile ├── PROJECT ├── README.md ├── RELEASING.md ├── api └── v1alpha2 │ ├── agentpool_helpers.go │ ├── agentpool_types.go │ ├── agentpool_validation.go │ ├── agentpool_validation_test.go │ ├── groupversion_info.go │ ├── helpers.go │ ├── helpers_test.go │ ├── module_types.go │ ├── module_validation.go │ ├── module_validation_test.go │ ├── project_helpers.go │ ├── project_helpers_test.go │ ├── project_types.go │ ├── project_validation.go │ ├── project_validation_test.go │ ├── validation.go │ ├── validation_test.go │ ├── workspace_helpers.go │ ├── workspace_types.go │ ├── workspace_validation.go │ ├── workspace_validation_test.go │ └── zz_generated.deepcopy.go ├── charts ├── hcp-terraform-operator │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── README.md.gotmpl │ ├── crds │ │ ├── app.terraform.io_agentpools.yaml │ │ ├── app.terraform.io_modules.yaml │ │ ├── app.terraform.io_projects.yaml │ │ └── app.terraform.io_workspaces.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── clusterrole_manager.yaml │ │ ├── clusterrole_metrics_reader.yaml │ │ ├── clusterrole_proxy.yaml │ │ ├── clusterrolebinding_manager.yaml │ │ ├── clusterrolebinding_proxy.yaml │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── role.yaml │ │ ├── rolebinding.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml └── test │ ├── go.mod │ ├── go.sum │ └── unit │ ├── deployment_test.go │ ├── helpers.go │ ├── rbac_clusster_role_binding_manager_test.go │ ├── rbac_clusster_role_binding_proxy_test.go │ ├── rbac_clusster_role_manager_test.go │ ├── rbac_clusster_role_metrics_reader_test.go │ ├── rbac_clusster_role_proxy_test.go │ ├── rbac_role_binding_test.go │ ├── rbac_role_test.go │ └── service_account_test.go ├── cmd └── main.go ├── config ├── crd │ ├── bases │ │ ├── app.terraform.io_agentpools.yaml │ │ ├── app.terraform.io_modules.yaml │ │ ├── app.terraform.io_projects.yaml │ │ └── app.terraform.io_workspaces.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_agentpools.yaml │ │ ├── cainjection_in_modules.yaml │ │ ├── cainjection_in_projects.yaml │ │ ├── cainjection_in_workspaces.yaml │ │ ├── webhook_in_agentpools.yaml │ │ ├── webhook_in_modules.yaml │ │ ├── webhook_in_projects.yaml │ │ └── webhook_in_workspaces.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── manifests │ ├── bases │ │ └── hcp-terraform-operator.clusterserviceversion.yaml │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── agentpool_editor_role.yaml │ ├── agentpool_viewer_role.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── module_editor_role.yaml │ ├── module_viewer_role.yaml │ ├── project_editor_role.yaml │ ├── project_viewer_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── service_account.yaml │ ├── workspace_editor_role.yaml │ └── workspace_viewer_role.yaml ├── samples │ ├── app_v1alpha2_agentpool.yaml │ ├── app_v1alpha2_module.yaml │ ├── app_v1alpha2_project.yaml │ ├── app_v1alpha2_workspace.yaml │ └── kustomization.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── docs ├── agentpool.md ├── annotations-and-labels.md ├── api-reference.md ├── config.yaml ├── examples │ ├── README.md │ ├── agentPool-basic.yaml │ ├── agentPool-deployment.yaml │ ├── module-basic.yaml │ ├── module-moduleName.yaml │ ├── module-variables-and-outputs.yaml │ ├── project-basic.yaml │ ├── project-customTeamAccess.yaml │ ├── project-teamAccess.yaml │ ├── workspace-basic.yaml │ ├── workspace-basicNotifications.yaml │ ├── workspace-project.yaml │ ├── workspace-runTasks.yaml │ ├── workspace-runTriggers.yaml │ ├── workspace-terraformVariables.yaml │ ├── workspace-terraformVersion.yaml │ └── workspace-variable-sets.yaml ├── faq.md ├── metrics.md ├── migration │ └── crds │ │ ├── workspaces_patch_a.yaml │ │ └── workspaces_patch_b.yaml ├── module.md ├── project.md ├── templates │ └── markdown │ │ ├── gv_details.tpl │ │ ├── gv_list.tpl │ │ ├── type.tpl │ │ └── type_members.tpl ├── usage.md └── workspace.md ├── go.mod ├── go.sum ├── hack ├── add-bundle-annotations.sh ├── add-bundle-replaces.sh └── boilerplate.go.txt ├── internal ├── controller │ ├── agentpool_controller.go │ ├── agentpool_controller_autoscaling.go │ ├── agentpool_controller_autoscaling_test.go │ ├── agentpool_controller_deletion_policy.go │ ├── agentpool_controller_deletion_policy_test.go │ ├── agentpool_controller_deployment.go │ ├── agentpool_controller_test.go │ ├── agentpool_controller_tokens.go │ ├── consts.go │ ├── flags.go │ ├── helpers.go │ ├── helpers_test.go │ ├── module_controller.go │ ├── module_controller_deletion_policy.go │ ├── module_controller_deletion_policy_test.go │ ├── module_controller_outputs.go │ ├── module_controller_test.go │ ├── module_controller_workspace.go │ ├── predicates.go │ ├── project_controller.go │ ├── project_controller_deletion_policy.go │ ├── project_controller_deletion_policy_test.go │ ├── project_controller_team_access.go │ ├── project_controller_team_access_test.go │ ├── project_controller_test.go │ ├── suite_test.go │ ├── workspace_controller.go │ ├── workspace_controller_agents.go │ ├── workspace_controller_agents_test.go │ ├── workspace_controller_deletion_policy.go │ ├── workspace_controller_deletion_policy_test.go │ ├── workspace_controller_notifications.go │ ├── workspace_controller_notifications_test.go │ ├── workspace_controller_outputs.go │ ├── workspace_controller_outputs_test.go │ ├── workspace_controller_projects.go │ ├── workspace_controller_projects_test.go │ ├── workspace_controller_remote_state_sharing.go │ ├── workspace_controller_remote_state_sharing_test.go │ ├── workspace_controller_run_tasks.go │ ├── workspace_controller_run_tasks_test.go │ ├── workspace_controller_run_triggers.go │ ├── workspace_controller_run_triggers_test.go │ ├── workspace_controller_runs.go │ ├── workspace_controller_runs_test.go │ ├── workspace_controller_sshkey.go │ ├── workspace_controller_sshkey_test.go │ ├── workspace_controller_tags.go │ ├── workspace_controller_tags_test.go │ ├── workspace_controller_team_access.go │ ├── workspace_controller_team_access_test.go │ ├── workspace_controller_test.go │ ├── workspace_controller_variable_sets.go │ ├── workspace_controller_variable_sets_test.go │ ├── workspace_controller_variables.go │ ├── workspace_controller_variables_test.go │ ├── workspace_controller_vcs.go │ └── workspace_controller_vcs_test.go ├── pointer │ ├── pointer.go │ └── pointer_test.go └── slice │ ├── slice.go │ └── slice_test.go ├── scripts └── update-helm-chart.sh └── version ├── VERSION └── version.go /.changes/2.4.0.md: -------------------------------------------------------------------------------- 1 | ## 2.4.0 (May 07, 2024) 2 | 3 | NOTES: 4 | 5 | * The `Workspace` CRD has been changed. Please follow the Helm chart instructions on how to upgrade it. [[GH-390](https://github.com/hashicorp/terraform-cloud-operator/pull/390)] 6 | * In upcoming releases, we shall proceed with renaming this project to HCP Terraform Operator for Kubernetes or simply HCP Terraform Operator. This measure is necessary in response to the recent announcement of [The Infrastructure Cloud](https://www.hashicorp.com/blog/introducing-the-infrastructure-cloud). The most noticeable change you can expect in version 2.6.0 is the renaming of this repository and related resources, such as the Helm chart and Docker Hub names. Please follow the changelogs for updates. [[GH-393](https://github.com/hashicorp/terraform-cloud-operator/pull/393)] 7 | 8 | BUG FIXES: 9 | 10 | * `Workspace`: Fix an issue when the controller panics while accessing the default Project. [[GH-394](https://github.com/hashicorp/terraform-cloud-operator/pull/394)] 11 | 12 | FEATURES: 13 | 14 | * `Workspace`: Add a new CLI option called `--workspace-sync-period` to set the time interval for re-queuing Workspace resources once they are successfully reconciled. [[GH-391](https://github.com/hashicorp/terraform-cloud-operator/pull/391)] 15 | * `Helm`: Add a new value called `controllers.workspace.syncPeriod` to set the CLI option `--workspace-sync-period`. [[GH-391](https://github.com/hashicorp/terraform-cloud-operator/pull/391)] 16 | 17 | ENHANCEMENTS: 18 | 19 | * `Workspace`: Update variables reconciliation logic to reduce the number of API calls. [[GH-390](https://github.com/hashicorp/terraform-cloud-operator/pull/390)] 20 | 21 | DEPENDENCIES: 22 | 23 | * Bump `github.com/hashicorp/go-tfe` from 1.47.1 to 1.49.0. [[GH-378](https://github.com/hashicorp/terraform-cloud-operator/pull/378)] 24 | * Bump `kube-rbac-proxy` from 0.16.0 to 0.17.0. [[GH-392](https://github.com/hashicorp/terraform-cloud-operator/pull/392)] 25 | * Bump `k8s.io/api` from 0.29.2 to 0.29.4. [[GH-399](https://github.com/hashicorp/terraform-cloud-operator/pull/399)] 26 | * Bump `k8s.io/apimachinery` from 0.29.2 to 0.29.4. [[GH-399](https://github.com/hashicorp/terraform-cloud-operator/pull/399)] 27 | * Bump `k8s.io/client-go` from 0.29.2 to 0.29.4. [[GH-399](https://github.com/hashicorp/terraform-cloud-operator/pull/399)] 28 | * Bump `sigs.k8s.io/controller-runtime` from 0.17.2 to 0.17.3. [[GH-399](https://github.com/hashicorp/terraform-cloud-operator/pull/399)] 29 | 30 | -------------------------------------------------------------------------------- /.changes/2.4.1.md: -------------------------------------------------------------------------------- 1 | ## 2.4.1 (June 07, 2024) 2 | 3 | NOTES: 4 | 5 | * In upcoming releases, we shall proceed with renaming this project to HCP Terraform Operator for Kubernetes or simply HCP Terraform Operator. This measure is necessary in response to the recent announcement of [The Infrastructure Cloud](https://www.hashicorp.com/blog/introducing-the-infrastructure-cloud). The most noticeable change you can expect in version 2.6.0 is the renaming of this repository and related resources, such as the Helm chart and Docker Hub names. Please follow the changelogs for updates. 6 | 7 | BUG FIXES: 8 | 9 | * `Module`: Fix an issue where the controller cannot create ConfigMap and Secret for outputs when a Module custom resource `metadata.name` is longer than 63 characters. This issue occurs because the controller uses the custom resource name as the value for the ModuleName label in ConfigMap and Secret. The `ModuleName` label has been removed." [[GH-423](https://github.com/hashicorp/terraform-cloud-operator/pull/423)] 10 | * `Workspace`: Fix an issue where the controller cannot create ConfigMap and Secret for outputs when a Workspace custom resource `metadata.name` is longer than 63 characters. This issue occurs because the controller uses the custom resource name as the value for the `workspaceName` label in ConfigMap and Secret. The `workspaceName` label has been removed. [[GH-423](https://github.com/hashicorp/terraform-cloud-operator/pull/423)] 11 | 12 | DEPENDENCIES: 13 | 14 | * Bump `github.com/hashicorp/go-tfe` from 1.49.0 to 1.55.0. [[GH-422](https://github.com/hashicorp/terraform-cloud-operator/pull/422)] 15 | * Bump `kube-rbac-proxy` from 0.17.0 to 0.18.0. [[GH-424](https://github.com/hashicorp/terraform-cloud-operator/pull/424)] 16 | 17 | ## Community Contributors :raised_hands: 18 | 19 | - @jtdoepke made their contribution in https://github.com/hashicorp/terraform-cloud-operator/pull/423 20 | - @nabadger for constantly providing us with a valuable feedback :rocket: 21 | 22 | -------------------------------------------------------------------------------- /.changes/2.5.0.md: -------------------------------------------------------------------------------- 1 | ## 2.5.0 (July 09, 2024) 2 | 3 | NOTES: 4 | 5 | * In upcoming releases, we shall proceed with renaming this project to HCP Terraform Operator for Kubernetes or simply HCP Terraform Operator. This measure is necessary in response to the recent announcement of [The Infrastructure Cloud](https://www.hashicorp.com/blog/introducing-the-infrastructure-cloud). The most noticeable change you can expect in version 2.6.0 is the renaming of this repository and related resources, such as the Helm chart and Docker Hub names. Please follow the changelogs for updates. 6 | 7 | FEATURES: 8 | 9 | * `Helm`: Add a new value called `controllers.agentPool.syncPeriod` to set the CLI option `--agent-pool-sync-period`. [[GH-421](https://github.com/hashicorp/terraform-cloud-operator/pull/421)] 10 | * `AgentPool`: Add a new CLI option called `--agent-pool-sync-period` to set the time interval for re-queuing Agent Pool resources once they are successfully reconciled. [[GH-421](https://github.com/hashicorp/terraform-cloud-operator/pull/421)] 11 | 12 | ENHANCEMENTS: 13 | 14 | * `AgentPool`: Update reconciliation logic to reduce the number of API calls. The controller now reconciles custom resources after the cooldown period if applicable; otherwise, the default timer is applied. [[GH-420](https://github.com/hashicorp/terraform-cloud-operator/pull/420)] 15 | * `AgentPool`: The agent auroscaling logic has been updated to decrease the frequency of API calls. The controller now utilizes the List Workspaces API call with filtering based on the current run status, thereby reducing the total number of API calls needed. [[GH-419](https://github.com/hashicorp/terraform-cloud-operator/pull/419)] 16 | * `Helm Chart`: Add the ability to configure the security context of the Deployment pod and containers. [[GH-432](https://github.com/hashicorp/terraform-cloud-operator/pull/432)] 17 | 18 | ## Community Contributors :raised_hands: 19 | 20 | - @vadim-kubasov made their contribution in https://github.com/hashicorp/terraform-cloud-operator/pull/432 21 | 22 | -------------------------------------------------------------------------------- /.changes/2.6.0.md: -------------------------------------------------------------------------------- 1 | ## 2.6.0 (July 30, 2024) 2 | 3 | NOTES: 4 | 5 | * The `AgentPool` CRD has been changed. Please follow the Helm chart instructions on how to upgrade it. [[GH-441](https://github.com/hashicorp/hcp-terraform-operator/pull/441)] 6 | 7 | BUG FIXES: 8 | 9 | * `Project`: Fix an issue where calls to paginated API endpoints were only fetching the first page of results. [[GH-426](https://github.com/hashicorp/hcp-terraform-operator/pull/426)] 10 | * `AgentPool`: Fix an issue where calls to paginated API endpoints were only fetching the first page of results. [[GH-426](https://github.com/hashicorp/hcp-terraform-operator/pull/426)] 11 | * `Workspace`: Fix an issue where calls to paginated API endpoints were only fetching the first page of results. [[GH-426](https://github.com/hashicorp/hcp-terraform-operator/pull/426)] 12 | 13 | ENHANCEMENTS: 14 | 15 | * `Helm Chart`: Add the ability to configure the service account. [[GH-431](https://github.com/hashicorp/hcp-terraform-operator/pull/431)] 16 | * `AgentPool`: Add the ability to configure scale-up and scale-down autoscaling times separately via the `cooldown.scaleUpSeconds` and `cooldown.scaleDownSeconds` attributes, respectively. [[GH-441](https://github.com/hashicorp/hcp-terraform-operator/pull/441)] 17 | 18 | DEPENDENCIES: 19 | 20 | * Bump `github.com/hashicorp/go-slug` from 0.15.0 to 0.15.2. [[GH-435](https://github.com/hashicorp/hcp-terraform-operator/pull/435)] 21 | * Bump `github.com/onsi/ginkgo/v2` from 2.16.0 to 2.19.0. [[GH-415](https://github.com/hashicorp/hcp-terraform-operator/pull/415)] 22 | * Bump `github.com/onsi/gomega` from 1.31.1 to 1.33.1. [[GH-415](https://github.com/hashicorp/hcp-terraform-operator/pull/415)] 23 | 24 | ## Community Contributors :raised_hands: 25 | 26 | - @frgray made their contribution in https://github.com/hashicorp/hcp-terraform-operator/pull/431 27 | - @jtdoepke made their contribution in https://github.com/hashicorp/hcp-terraform-operator/pull/426 28 | - @omelnyk1 for sharing his Helm expertise and valuable feedback 🚀 29 | 30 | -------------------------------------------------------------------------------- /.changes/2.6.1.md: -------------------------------------------------------------------------------- 1 | ## 2.6.1 (August 07, 2024) 2 | 3 | BUG FIXES: 4 | 5 | * `Workspace`: Fix an issue where the controller fails to update CR Status when CR gets modified during the reconciliation. [[GH-457](https://github.com/hashicorp/hcp-terraform-operator/pull/457)] 6 | * `Workspace`: Fix an issue where, in some circumstances, the controller cannot properly handle the deletion event. [[GH-460](https://github.com/hashicorp/hcp-terraform-operator/pull/460)] 7 | 8 | ENHANCEMENTS: 9 | 10 | * `Helm Chart`: Add the ability to configure the Deployment priority class. [[GH-451](https://github.com/hashicorp/hcp-terraform-operator/pull/451)] 11 | 12 | ## Community Contributors :raised_hands: 13 | 14 | - @vadim-kubasov made their contribution in https://github.com/hashicorp/hcp-terraform-operator/pull/451 15 | 16 | -------------------------------------------------------------------------------- /.changes/2.7.1.md: -------------------------------------------------------------------------------- 1 | ## 2.7.1 (December 04, 2024) 2 | 3 | BREAKING CHANGES: 4 | 5 | * `Helm Chart`: The `customCAcertificates` value has been replaced to accept a base64-encoded CA bundle instead of a file path. This change aims to simplify the installation/upgrade workflow. [[GH-516](https://github.com/hashicorp/hcp-terraform-operator/pull/516)] 6 | 7 | NOTES: 8 | 9 | * `Helm Chart`: The default value of `operator.syncPeriod` has changed from 5 minutes to 1 hour to reduce unnecessary reconciliation. [[GH-512](https://github.com/hashicorp/hcp-terraform-operator/pull/512)] 10 | 11 | BUG FIXES: 12 | 13 | * `Workspace`: Fix an issue where `spec.agentPool` can be set even when `spec.executionMode` is not set to `agent`. [[GH-504](https://github.com/hashicorp/hcp-terraform-operator/pull/504)] 14 | * `Helm Chart`: Fix an issue that prevented custom CA certificates from being attached to the pod volume. The `customCAcertificates` value now refers to a base64-encoded CRT bundle instead of a file path. [[GH-516](https://github.com/hashicorp/hcp-terraform-operator/pull/516)] 15 | 16 | ENHANCEMENTS: 17 | 18 | * `Helm Chart`: Add the ability to configure `affinity` and `tolerations` for the Deployment of the operator. [[GH-495](https://github.com/hashicorp/hcp-terraform-operator/pull/495)] 19 | * `Helm Chart`: Add the ability to configure additional labels for the Operator pod. [[GH-522](https://github.com/hashicorp/hcp-terraform-operator/pull/522)] 20 | 21 | DEPENDENCIES: 22 | 23 | * Bump `kube-rbac-proxy` from 0.18.0 to 0.18.2. [[GH-514](https://github.com/hashicorp/hcp-terraform-operator/pull/514)] [[GH-531](https://github.com/hashicorp/hcp-terraform-operator/pull/531)] 24 | * Bump `github.com/hashicorp/go-tfe` from 1.62.0 to 1.71.0. [[GH-508](https://github.com/hashicorp/hcp-terraform-operator/pull/508)] [[GH-532](https://github.com/hashicorp/hcp-terraform-operator/pull/532)] 25 | * Bump `github.com/hashicorp/go-slug` from 0.15.2 to 0.16.1. [[GH-508](https://github.com/hashicorp/hcp-terraform-operator/pull/508)] [[GH-519](https://github.com/hashicorp/hcp-terraform-operator/pull/519)] 26 | * Bump `k8s.io/api` from 0.30.3 to 0.31.3. [[GH-525](https://github.com/hashicorp/hcp-terraform-operator/pull/525)] [[GH-527](https://github.com/hashicorp/hcp-terraform-operator/pull/527)] 27 | * Bump `k8s.io/apimachinery` from 0.30.3 to 0.31.3. [[GH-525](https://github.com/hashicorp/hcp-terraform-operator/pull/525)] [[GH-526](https://github.com/hashicorp/hcp-terraform-operator/pull/526)] 28 | * Bump `sigs.k8s.io/controller-runtime` from 0.18.4 to 0.19.2. [[GH-525](https://github.com/hashicorp/hcp-terraform-operator/pull/525)] 29 | * Bump `k8s.io/client-go` from 0.30.3 to 0.31.3. [[GH-525](https://github.com/hashicorp/hcp-terraform-operator/pull/525)] [[GH-527](https://github.com/hashicorp/hcp-terraform-operator/pull/527)] 30 | 31 | ## Community Contributors :raised_hands: 32 | 33 | - @baptman21 made their contribution in https://github.com/hashicorp/hcp-terraform-operator/pull/495 34 | - @mlflr made their contribution in https://github.com/hashicorp/hcp-terraform-operator/pull/522 35 | 36 | -------------------------------------------------------------------------------- /.changes/2.8.0.md: -------------------------------------------------------------------------------- 1 | ## 2.8.0 (February 10, 2025) 2 | 3 | NOTES: 4 | 5 | * The `Workspace` CRD has been changed. Please follow the Helm chart instructions on how to upgrade it. [[GH-497](https://github.com/hashicorp/hcp-terraform-operator/pull/497)] 6 | 7 | ENHANCEMENTS: 8 | 9 | * `Workspace`: Add the ability to attach variable sets to a workspace via a new optional field `spec.variableSets`. [[GH-497](https://github.com/hashicorp/hcp-terraform-operator/pull/497)] 10 | 11 | DEPENDENCIES: 12 | 13 | * Bump `github.com/hashicorp/go-slug` from 0.16.1 to 0.16.3. [[GH-549](https://github.com/hashicorp/hcp-terraform-operator/pull/549)] 14 | 15 | -------------------------------------------------------------------------------- /.changes/2.8.1.md: -------------------------------------------------------------------------------- 1 | ## 2.8.1 (March 12, 2025) 2 | 3 | ENHANCEMENTS: 4 | 5 | * `Workspace`: Add support for attaching variable sets to a workspace referenced by its name. [[GH-570](https://github.com/hashicorp/hcp-terraform-operator/pull/570)] 6 | 7 | DEPENDENCIES: 8 | 9 | * Bump `kube-rbac-proxy` from 0.18.2 to 0.19.0. [[GH-560](https://github.com/hashicorp/hcp-terraform-operator/pull/560)] 10 | * Bump `github.com/hashicorp/go-tfe` from 1.71.0 to 1.76.0. [[GH-566](https://github.com/hashicorp/hcp-terraform-operator/pull/566)] 11 | * Bump `github.com/hashicorp/go-slug` from 0.16.3 to 0.16.4. [[GH-566](https://github.com/hashicorp/hcp-terraform-operator/pull/566)] 12 | * Bump `k8s.io/api` from 0.31.3 to 0.31.6. [[GH-571](https://github.com/hashicorp/hcp-terraform-operator/pull/571)] 13 | * Bump `k8s.io/apimachinery` from 0.31.3 to 0.31.6. [[GH-571](https://github.com/hashicorp/hcp-terraform-operator/pull/571)] 14 | * Bump `k8s.io/client-go` from 0.31.3 to 0.31.6. [[GH-571](https://github.com/hashicorp/hcp-terraform-operator/pull/571)] 15 | * Bump `sigs.k8s.io/controller-runtimeg` from 0.19.2 to 0.19.7. [[GH-571](https://github.com/hashicorp/hcp-terraform-operator/pull/571)] 16 | 17 | -------------------------------------------------------------------------------- /.changes/2.9.1.md: -------------------------------------------------------------------------------- 1 | ## 2.9.1 (May 14, 2025) 2 | 3 | BUG FIXES: 4 | 5 | * Fixed an issue where the operator could not connect to the HCP Terraform / TFE instance when using the UBI-based image due to a TLS validation error. The previous workaround required setting the `TFC_TLS_SKIP_VERIFY` environment variable to `true` in the Deployment. [[GH-600](https://github.com/hashicorp/hcp-terraform-operator/pull/600)] 6 | 7 | ENHANCEMENTS: 8 | 9 | * `Helm Chart`: Add the ability to configure environment variables for the Operator Deployment via `operator.env`. [[GH-601](https://github.com/hashicorp/hcp-terraform-operator/pull/601)] 10 | 11 | DEPENDENCIES: 12 | 13 | * Bump `kube-rbac-proxy` from 0.19.0 to 0.19.1. [[GH-599](https://github.com/hashicorp/hcp-terraform-operator/pull/599)] 14 | 15 | -------------------------------------------------------------------------------- /.changes/2.9.2.md: -------------------------------------------------------------------------------- 1 | ## 2.9.2 (May 28, 2025) 2 | 3 | BUG FIXES: 4 | 5 | * Fix an issue where the agent can be terminated while it still has an active run during the post-plan or post-apply stage, such as, but not limited to, Sentinel policy evaluation. [[GH-610](https://github.com/hashicorp/hcp-terraform-operator/pull/610)] 6 | 7 | ## Community Contributors :raised_hands: 8 | 9 | - @munnep identified and successfully reproduced the issue. Great work tracking it down! https://github.com/hashicorp/hcp-terraform-operator/pull/610 10 | 11 | -------------------------------------------------------------------------------- /.changes/unreleased/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/hcp-terraform-operator/546c55dc6a7ead76f1d2247939cf42a3a9d6be87/.changes/unreleased/.gitkeep -------------------------------------------------------------------------------- /.changie.yaml: -------------------------------------------------------------------------------- 1 | changesDir: .changes 2 | unreleasedDir: unreleased 3 | changelogPath: CHANGELOG.md 4 | versionExt: md 5 | versionFormat: '## {{.Version}} ({{.Time.Format "January 02, 2006"}})' 6 | fragmentFileFormat: '{{.Kind}}-{{.Custom.PR}}-{{.Time.Format "20060102-150405"}}' 7 | kindFormat: '{{.Kind}}:' 8 | changeFormat: '* {{.Body}} [[GH-{{.Custom.PR}}](https://github.com/hashicorp/hcp-terraform-operator/pull/{{.Custom.PR}})]' 9 | custom: 10 | - key: PR 11 | label: PR Number 12 | type: int 13 | minInt: 1 14 | kinds: 15 | - label: BREAKING CHANGES 16 | auto: minor 17 | - label: NOTES 18 | auto: minor 19 | - label: BUG FIXES 20 | auto: patch 21 | - label: FEATURES 22 | auto: minor 23 | - label: ENHANCEMENTS 24 | auto: minor 25 | - label: DEPENDENCIES 26 | auto: minor 27 | newlines: 28 | afterKind: 1 29 | beforeKind: 1 30 | endOfVersion: 2 31 | -------------------------------------------------------------------------------- /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2022 6 | header_ignore = [ 7 | ".github/**", 8 | # All files within the config directory are generated automatically by the operator-sdk CLI tool 9 | # Some files were scaffolded during the first run of the operator-sdk CLI tool and never changed 10 | # The dev team is not working with these files they all serve a supporting role 11 | "config/**", 12 | # Changie is a tool to manage changelog entries 13 | ".changes/unreleased/*.yaml", 14 | ".changie.yaml", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Main codeowners 2 | * @hashicorp/tf-eco-hybrid-cloud 3 | 4 | # Release configuration 5 | /.release/ @hashicorp/tf-eco-hybrid-cloud 6 | /.github/workflows/build.yml @hashicorp/tf-eco-hybrid-cloud 7 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | title: '🐛 ' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 12 | ### Operator Version, Kind and Kubernetes Version 13 | - Operator version: 14 | - Kind: Workspace | Module 15 | - Kubernetes version: 16 | 17 | ### YAML Manifest File 18 | ```yaml 19 | # Copy-paste your YAML manifest here 20 | ``` 21 | 22 | ### Output Log 23 | 27 | 28 | ### Output of relevant `kubectl` commands 29 | 33 | 34 | ### Steps To Reproduce 35 | 41 | 42 | ### Expected Behavior 43 | What should have happened? 44 | 45 | ### Actual Behavior 46 | What actually happened? 47 | 48 | ### Additional Context 49 | Add any other context about the problem here. 50 | 51 | ### References 52 | 57 | 58 | ### Community Note 59 | 60 | * Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request. 61 | * Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. 62 | * If you are interested in working on this issue or have submitted a pull request, please leave a comment. 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: Suggest an idea or an improvement for this project 4 | title: '🚀 ' 5 | labels: 'feature-request' 6 | assignees: '' 7 | 8 | --- 9 | 12 | ### Description 13 | 16 | 17 | ### Potential YAML Configuration 18 | ```yaml 19 | ``` 20 | 21 | ### References 22 | 27 | 28 | 29 | ### Community Note 30 | 31 | * Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request. 32 | * Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. 33 | * If you are interested in working on this issue or have submitted a pull request, please leave a comment. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🤔 Question 3 | about: If you need help figuring something out 4 | title: '🤔 ' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 12 | ### Operator Version, Kind and Kubernetes Version 13 | - Operator version: 14 | - Kind: Workspace | Module 15 | - Kubernetes version: 16 | 17 | ### YAML Manifest File 18 | 21 | ```yaml 22 | # Copy-paste your YAML manifest here 23 | ``` 24 | 25 | ### Output Log 26 | 29 | 30 | ### Kubectl Outputs 31 | 34 | 35 | ### Question 36 | 39 | 40 | ### References 41 | 46 | 47 | ### Community Note 48 | 49 | * Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request. 50 | * Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. 51 | * If you are interested in working on this issue or have submitted a pull request, please leave a comment. 52 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | We deeply appreciate any effort to discover and disclose security vulnerabilities responsibly. 2 | 3 | If you would like to report a vulnerability in one of our products, or have security concerns regarding HashiCorp software, please email [security@hashicorp.com](mailto:security@hashicorp.com). 4 | 5 | In order for us to best respond to your report, please include any of the following: 6 | 7 | * Steps to reproduce or proof-of-concept 8 | * Any relevant tools, including versions used 9 | * Tool output 10 | 11 | For additional information about HashiCorp security, please see [https://hashicorp.com/security](https://hashicorp.com/security). 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | commit-message: 8 | prefix: "🌱" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | # DependaBot does not open a pull request to update the version of GH actions, only security updates. 14 | # TSCCR is responsible for opening a pull request to update the version of GH actions. 15 | open-pull-requests-limit: 0 16 | commit-message: 17 | prefix: "🤖" 18 | -------------------------------------------------------------------------------- /.github/hcp-terraform-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/hcp-terraform-operator/546c55dc6a7ead76f1d2247939cf42a3a9d6be87/.github/hcp-terraform-logo.png -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | api: 2 | - any: 3 | - changed-files: 4 | - any-glob-to-any-file: 5 | - 'api/**/*.go' 6 | 7 | controller: 8 | - any: 9 | - changed-files: 10 | - any-glob-to-any-file: 11 | - 'controllers/*.go' 12 | 13 | crd: 14 | - any: 15 | - changed-files: 16 | - any-glob-to-any-file: 17 | - 'config/crd/bases/*.yaml' 18 | 19 | dependencies: 20 | - any: 21 | - changed-files: 22 | - any-glob-to-any-file: 23 | - 'go.mod' 24 | - 'go.sum' 25 | 26 | documentation: 27 | - all: 28 | - changed-files: 29 | - any-glob-to-any-file: 30 | - '*.md' 31 | - 'charts/**/*.md' 32 | - 'docs/*.md' 33 | - all-globs-to-all-files: 34 | - '!CHANGELOG.md' 35 | 36 | github_actions: 37 | - any: 38 | - changed-files: 39 | - any-glob-to-any-file: 40 | - '**/workflows/*.yaml' 41 | - '**/workflows/*.yml' 42 | 43 | golang: 44 | - any: 45 | - changed-files: 46 | - any-glob-to-any-file: 47 | - '**/*.go' 48 | 49 | helm-chart: 50 | - any: 51 | - changed-files: 52 | - any-glob-to-any-file: 53 | - 'charts/**/*.yaml' 54 | 55 | release: 56 | - any: 57 | - changed-files: 58 | - any-glob-to-any-file: 59 | - 'version/VERSION' 60 | 61 | test: 62 | - any: 63 | - changed-files: 64 | - any-glob-to-any-file: 65 | - '**/*_test.go' 66 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | ## Rollback Plan 16 | 17 | If a change needs to be reverted, we will publish an updated version of the library. 18 | 19 | ## Changes to Security Controls 20 | 21 | Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain. 22 | 23 | 26 | 27 | ### Description 28 | 29 | 32 | 33 | ### Usage Example 34 | 35 | 38 | 39 | ### References 40 | 41 | 46 | 47 | ### Community Note 48 | 49 | * Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request. 50 | * Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. 51 | * If you are interested in working on this issue or have submitted a pull request, please leave a comment. 52 | -------------------------------------------------------------------------------- /.github/test-infra/tfe/README.md: -------------------------------------------------------------------------------- 1 | # EXAMPLE: Standalone Mounted Installation of Terraform Enterprise 2 | 3 | ## About This Example 4 | 5 | This example for Terraform Enterprise creates a TFE installation with the following traits: 6 | 7 | - Standalone 8 | - Mounted Disk production type 9 | - n1-standard-4 virtual machine type 10 | - Ubuntu 20.04 11 | - A publicly accessible HTTP load balancer with TLS termination 12 | 13 | 14 | ## Prerequisites 15 | 16 | This example assumes that the following resources exist: 17 | 18 | - TFE license is on a file path defined by `var.license_file` 19 | - A DNS zone (already provisioned and speficied in the auto.tfvars file) 20 | - Valid managed SSL certificate to use with load balancer (already provisioned and speficied in the auto.tfvars file) 21 | 22 | ## How to Use This Module 23 | 24 | ### Deployment 25 | 26 | 1. Read the entire [README.md](../../README.md) of the root module. 27 | 2. Ensure account meets module prerequisites from above. 28 | 3. Clone repository. 29 | 4. Change directory into desired example folder. 30 | 5. Create a local `terraform.auto.tfvars` file and instantiate the required inputs as in the respective `./examples/standalone-mounted/variables.tf` including the path to the license under the `license_file` variable value. 31 | 6. Authenticate against the Google provider. See [instructions](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#authentication). 32 | 7. Initialize terraform and apply the module configurations using the commands below: 33 | 34 | NOTE: `terraform plan` will print out the execution plan which describes the actions Terraform will take in order to build your infrastructure to match the module configuration. If anything in the plan seems incorrect or dangerous, it is safe to abort here and not proceed to `terraform apply`. 35 | 36 | ``` 37 | terraform init 38 | terraform plan 39 | terraform apply -auto-approve 40 | ``` 41 | 42 | ## Post-deployment Tasks 43 | 44 | The build should take approximately 10-15 minutes to deploy. Once the module has completed, give the platform another 10 minutes or so prior to attempting to interact with it in order for all containers to start up. 45 | 46 | Next comes creating the admin user. To do this, use the Terraform configuration in the `./initial-admin` sub-directory. 47 | DO NOT run a PLAN on this configuration. This is because the admin creation API calls are performed via datasource, which actually evaluate at plan time. Effectively, the admin user is created during the planning phase. Just run ONE SINGLE APPLY, that implies a single plan action. 48 | 49 | WARNING: please not the absence of the plan action!!! DO NOT RUN PLAN! 50 | 51 | ``` 52 | terraform init 53 | terraform apply -auto-approve 54 | ``` 55 | 56 | The output of this apply will print out the API token corresponding to the admin user. 57 | 58 | ### Connecting to the TFE Console 59 | 60 | The TFE Console is only available in a standalone environment 61 | 62 | 1. Navigate to the URL supplied via `tfe_console_url` Terraform output 63 | 2. Copy the `tfe_console_password` Terraform output 64 | 3. Enter the console password 65 | 4. Click `Unlock` 66 | 67 | ### Connecting to the TFE Application 68 | 69 | 1. Navigate to the URL supplied via the `login_url` Terraform output. (It may take several minutes for this to be available after initial deployment. You may monitor the progress of cloud init if desired on one of the instances.) 70 | 2. Enter a `username`, `email`, and `password` for the initial user. 71 | 3. Click `Create an account`. 72 | 4. After the initial user is created you may access the TFE Application normally using the URL supplied via `login_url` Terraform output. -------------------------------------------------------------------------------- /.github/test-infra/tfe/initial-admin/admin.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | variable "admin_email" { 5 | type = string 6 | default = "tf-strategic@hashicorp.com" 7 | } 8 | 9 | data "terraform_remote_state" "tfe" { 10 | backend = "local" 11 | 12 | config = { 13 | path = "../terraform.tfstate" 14 | } 15 | } 16 | 17 | # Wait for the TFE installation to finish internal provisioning on the node and report itself as available. 18 | # 19 | data "http" "wait-for-ok" { 20 | url = data.terraform_remote_state.tfe.outputs.health_check_url 21 | retry { 22 | attempts = 2000 23 | min_delay_ms = 1000 24 | } 25 | } 26 | 27 | # Grab the time-limited IACT token required to create the admin user. 28 | # 29 | data "http" "iact_token" { 30 | url = data.terraform_remote_state.tfe.outputs.iact_url 31 | retry { 32 | attempts = 2000 33 | min_delay_ms = 1000 34 | } 35 | } 36 | 37 | # Create the admin user and retrieve its associated token 38 | # 39 | data "http" "admin_user_token" { 40 | url = "${data.terraform_remote_state.tfe.outputs.initial_admin_user_url}?token=${data.http.iact_token.response_body}" 41 | method = "POST" 42 | request_headers = { 43 | Content-Type = "application/json" 44 | } 45 | request_body = jsonencode({ 46 | username = "admin" 47 | password = data.terraform_remote_state.tfe.outputs.replicated_console_password 48 | email = var.admin_email 49 | }) 50 | } 51 | 52 | output "console_url" { 53 | value = data.terraform_remote_state.tfe.outputs.url 54 | } 55 | 56 | output "admin_password" { 57 | value = data.terraform_remote_state.tfe.outputs.replicated_console_password 58 | } 59 | 60 | output "admin_token" { 61 | value = jsondecode(data.http.admin_user_token.response_body).token 62 | } 63 | -------------------------------------------------------------------------------- /.github/test-infra/tfe/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Random String for unique names 5 | # ------------------------------ 6 | resource "random_pet" "main" { 7 | length = 1 8 | } 9 | 10 | # Store TFE License as secret 11 | # --------------------------- 12 | module "secrets" { 13 | source = "github.com/hashicorp/terraform-google-terraform-enterprise/fixtures/secrets" 14 | 15 | license = { 16 | id = random_pet.main.id 17 | path = var.license_file 18 | } 19 | } 20 | 21 | # Gets the external IP address of the provisioner 22 | # TFE requires this in order to accept admin creation API calls 23 | # ------------------------------------------------------------- 24 | data "http" "icanhazip" { 25 | url = "http://icanhazip.com" 26 | } 27 | 28 | # Standalone, mounted disk 29 | # ------------------------ 30 | module "tfe" { 31 | source = "github.com/hashicorp/terraform-google-terraform-enterprise" 32 | 33 | distribution = "ubuntu" 34 | dns_zone_name = var.dns_zone_name 35 | existing_service_account_id = var.existing_service_account_id 36 | namespace = random_pet.main.id 37 | node_count = 1 38 | fqdn = var.fqdn 39 | load_balancer = "PUBLIC" 40 | ssl_certificate_name = var.ssl_certificate_name 41 | tfe_license_secret_id = module.secrets.license_secret 42 | vm_machine_type = "n1-standard-4" 43 | iact_subnet_list = tolist(["${chomp(data.http.icanhazip.body)}/32"]) 44 | } 45 | -------------------------------------------------------------------------------- /.github/test-infra/tfe/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | output "health_check_url" { 5 | value = module.tfe.health_check_url 6 | description = "The URL of the Terraform Enterprise health check endpoint." 7 | } 8 | 9 | output "iact_notice" { 10 | value = "Once deployed, please follow this page to set the initial user up: https://www.terraform.io/docs/enterprise/install/automating-initial-user.html" 11 | description = "Login advice message." 12 | } 13 | 14 | output "iact_url" { 15 | value = module.tfe.iact_url 16 | description = "IACT URL" 17 | } 18 | 19 | output "initial_admin_user_url" { 20 | value = module.tfe.initial_admin_user_url 21 | description = "Initial Admin user URL" 22 | } 23 | 24 | output "lb_address" { 25 | value = module.tfe.lb_address 26 | description = "Load Balancer Address" 27 | } 28 | 29 | output "replicated_console_password" { 30 | value = module.tfe.replicated_console_password 31 | description = "Generated password for replicated dashboard" 32 | } 33 | 34 | output "url" { 35 | value = module.tfe.url 36 | description = "Login URL to setup the TFE instance once it is initialized" 37 | } -------------------------------------------------------------------------------- /.github/test-infra/tfe/terraform.auto.tfvars: -------------------------------------------------------------------------------- 1 | existing_service_account_id = "tfe-k8s-dev" 2 | fqdn = "tfe.gcp.terraform-k8s-providers-ci.hashicorp.services" 3 | dns_zone_name = "gcp-terraform-k8s-providers-ci" 4 | ssl_certificate_name = "tfe-api-k8s-20230503150723712400000001" 5 | -------------------------------------------------------------------------------- /.github/test-infra/tfe/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | variable "dns_zone_name" { 5 | description = "The name of the DNS zone in which a record will be created." 6 | type = string 7 | } 8 | 9 | variable "existing_service_account_id" { 10 | default = null 11 | type = string 12 | description = "The ID of the logging service account to use for compute resources deployed." 13 | } 14 | 15 | variable "fqdn" { 16 | description = "The fully qualified domain name which will be assigned to the DNS record." 17 | type = string 18 | } 19 | 20 | variable "license_file" { 21 | type = string 22 | description = "The local path to the Terraform Enterprise license." 23 | } 24 | 25 | variable "ssl_certificate_name" { 26 | description = <<-EOD 27 | The name of an existing SSL certificate which will be used to authenticate connections to the load balancer. 28 | EOD 29 | type = string 30 | } 31 | -------------------------------------------------------------------------------- /.github/test-infra/tfe/versions.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | terraform { 5 | required_version = ">= 0.14" 6 | 7 | required_providers { 8 | random = { 9 | source = "hashicorp/random" 10 | version = "~> 3.0" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code Scanning - Action" 2 | 3 | on: 4 | schedule: 5 | - cron: '30 3 * * 0' 6 | pull_request: 7 | branches: 8 | - main 9 | push: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | permissions: 19 | security-events: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 27 | with: 28 | go-version-file: 'go.mod' 29 | 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@99c9897648dded3fe63d6f328c46089dd57735ca # codeql-bundle-v2.17.0 32 | with: 33 | languages: go 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@99c9897648dded3fe63d6f328c46089dd57735ca # codeql-bundle-v2.17.0 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@99c9897648dded3fe63d6f328c46089dd57735ca # codeql-bundle-v2.17.0 40 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Dependabot PR 2 | 3 | on: 4 | # https://github.com/dependabot/dependabot-core/issues/3253#issuecomment-852541544 5 | pull_request_target: 6 | branches: 7 | - '**' 8 | paths: 9 | - 'go.mod' 10 | - 'go.sum' 11 | 12 | jobs: 13 | hcp-terraform: 14 | if: github.actor == 'dependabot[bot]' 15 | uses: ./.github/workflows/end-to-end-tfc.yaml 16 | hcp-terraform-helm: 17 | if: github.actor == 'dependabot[bot]' 18 | uses: ./.github/workflows/helm-end-to-end-tfc.yaml 19 | terraform-enterprise: 20 | if: github.actor == 'dependabot[bot]' 21 | uses: ./.github/workflows/end-to-end-tfe.yaml 22 | terraform-enterprise-helm: 23 | if: github.actor == 'dependabot[bot]' 24 | uses: ./.github/workflows/helm-end-to-end-tfe.yaml 25 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Scan Docker image for vulnerabilities 2 | 3 | on: 4 | schedule: 5 | - cron: '30 7 * * *' 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | scan-docker-image: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Docker image metadata 16 | id: meta 17 | uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 18 | with: 19 | images: operator 20 | tags: | 21 | type=sha,format=long 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 25 | with: 26 | platforms: amd64 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 30 | 31 | - name: Build and load Docker image 32 | uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 33 | with: 34 | platforms: linux/amd64 35 | push: false 36 | load: true 37 | build-args: 38 | BIN_NAME=${{ vars.BIN_NAME }} 39 | tags: ${{ steps.meta.outputs.tags }} 40 | 41 | - name: Run Trivy vulnerability scanner 42 | uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # 0.29.0 43 | with: 44 | scan-type: image 45 | format: sarif 46 | image-ref: ${{ steps.meta.outputs.tags }} 47 | output: 'trivy-results.sarif' 48 | exit-code: '1' 49 | 50 | - name: Upload Trivy scan results to GitHub Security tab 51 | uses: github/codeql-action/upload-sarif@c4fb451437765abf5018c6fbf22cce1a7da1e5cc # codeql-bundle-v2.17.1 52 | with: 53 | category: 'Trivy Security Scan' 54 | sarif_file: 'trivy-results.sarif' 55 | -------------------------------------------------------------------------------- /.github/workflows/end-to-end-tfc.yaml: -------------------------------------------------------------------------------- 1 | name: E2E on HCP Terraform Operator 2 | 3 | 4 | on: 5 | schedule: 6 | - cron: '30 5 * * 0' 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - 'internal/controller/**' 12 | workflow_dispatch: 13 | workflow_call: 14 | 15 | jobs: 16 | tests: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 26 | with: 27 | go-version-file: 'go.mod' 28 | 29 | - name: Run end-to-end test suite 30 | run: make test 31 | env: 32 | TFC_OAUTH_TOKEN: ${{ secrets.TFC_OAUTH_TOKEN }} 33 | TFC_ORG: ${{ secrets.TFC_ORG }} 34 | TFC_TOKEN: ${{ secrets.TFC_TOKEN }} 35 | TFC_VCS_REPO: ${{ secrets.TFC_VCS_REPO }} 36 | -------------------------------------------------------------------------------- /.github/workflows/end-to-end-tfe.yaml: -------------------------------------------------------------------------------- 1 | name: E2E on Terraform Enterprise 2 | 3 | on: 4 | schedule: 5 | - cron: '30 5 * * 0' 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - 'internal/controller/**' 11 | workflow_dispatch: 12 | workflow_call: 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup Go 24 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 25 | with: 26 | go-version-file: 'go.mod' 27 | 28 | - name: Run end-to-end test suite 29 | run: make test 30 | env: 31 | TFC_OAUTH_TOKEN: ${{ secrets.TFE_OAUTH_TOKEN }} 32 | TFC_ORG: ${{ secrets.TFE_ORG }} 33 | TFC_TOKEN: ${{ secrets.TFE_TOKEN }} 34 | TFC_VCS_REPO: ${{ secrets.TFE_VCS_REPO }} 35 | TFE_ADDRESS: ${{ secrets.TFE_ADDRESS }} 36 | TFC_TLS_SKIP_VERIFY: true 37 | -------------------------------------------------------------------------------- /.github/workflows/hc-copywrite.yml: -------------------------------------------------------------------------------- 1 | name: "HashiCorp Copywrite" 2 | 3 | on: 4 | schedule: 5 | - cron: '30 2 * * *' 6 | pull_request: 7 | branches: 8 | - main 9 | push: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | copywrite: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 20 | 21 | - name: Install copywrite 22 | uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3 23 | 24 | - name: Validate Header Compliance 25 | run: copywrite headers --plan 26 | -------------------------------------------------------------------------------- /.github/workflows/helm-chart-unit.yaml: -------------------------------------------------------------------------------- 1 | name: Helm Chart Unit Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - 'charts/**' 9 | - 'version/VERSION' 10 | push: 11 | branches: 12 | - main 13 | workflow_dispatch: 14 | 15 | env: 16 | HELM_CHART_PATH: 'charts/hcp-terraform-operator' 17 | 18 | jobs: 19 | tests: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 30 | with: 31 | go-version-file: 'go.mod' 32 | 33 | - name: Set up Helm 34 | uses: Azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 35 | with: 36 | version: v3.11.2 37 | 38 | - name: Run unit tests suite [Go] 39 | run: | 40 | make helm-test 41 | -------------------------------------------------------------------------------- /.github/workflows/helm-docs.yml: -------------------------------------------------------------------------------- 1 | name: "Helm Chart Documentation" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - 'charts/hcp-terraform-operator/**' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | helm-docs: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 20 | with: 21 | go-version-file: 'go.mod' 22 | 23 | - name: Run Helm documentation generator 24 | run: make helm-docs 25 | 26 | - name: Validate changes 27 | run: git diff --exit-code 28 | 29 | - name: Uncommited changes 30 | if: ${{ failure() }} 31 | run: | 32 | echo "There are uncommitted changes in Helm Chart Documentation. Please run 'make helm-docs'." 33 | exit 1 34 | 35 | - name: Green light 36 | if: ${{ success() }} 37 | run: | 38 | echo "Helm Chart Documentation is up to date." 39 | exit 0 40 | -------------------------------------------------------------------------------- /.github/workflows/helm-end-to-end-tfc.yaml: -------------------------------------------------------------------------------- 1 | name: E2E on HCP Terraform Operator [Helm] 2 | 3 | on: 4 | schedule: 5 | - cron: '30 6 * * 0' 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - 'internal/controller/**' 11 | - 'charts/hcp-terraform-operator/**' 12 | workflow_dispatch: 13 | workflow_call: 14 | 15 | env: 16 | USE_EXISTING_CLUSTER: true 17 | CLUSTER_NAME: 'this' 18 | DOCKER_IMAGE: 'this' 19 | KUBECONFIG: ${{ github.workspace }}/.kube/config 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 33 | with: 34 | go-version-file: 'go.mod' 35 | 36 | - name: Set up kind 37 | uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 38 | with: 39 | wait: 2m 40 | version: v${{ vars.KIND_VERSION }} 41 | cluster_name: ${{ env.CLUSTER_NAME }} 42 | 43 | - name: Set up Helm 44 | uses: Azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 45 | with: 46 | version: v3.11.2 47 | 48 | - name: Generate Docker image metadata 49 | id: meta 50 | uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 51 | with: 52 | images: ${{ env.DOCKER_IMAGE }} 53 | tags: | 54 | type=sha,prefix=,format=short 55 | 56 | - name: Set up QEMU 57 | uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 58 | with: 59 | platforms: amd64 60 | 61 | - name: Set up Docker Buildx 62 | uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 63 | 64 | - name: Build and load Docker image 65 | uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 66 | with: 67 | platforms: linux/amd64 68 | load: true 69 | build-args: 70 | BIN_NAME=${{ vars.BIN_NAME }} 71 | tags: ${{ env.DOCKER_METADATA_OUTPUT_TAGS }} 72 | 73 | - name: Upload Docker image to kind 74 | run: | 75 | kind load docker-image ${{ env.DOCKER_METADATA_OUTPUT_TAGS }} --name ${{ env.CLUSTER_NAME }} 76 | 77 | - name: Install Helm chart 78 | run: | 79 | helm install --wait --timeout 1m this ./charts/hcp-terraform-operator \ 80 | --set operator.image.repository=${{ env.DOCKER_IMAGE }} \ 81 | --set operator.image.tag=${{ env.DOCKER_METADATA_OUTPUT_VERSION }} \ 82 | --set operator.syncPeriod=30s \ 83 | --set controllers.agentPool.workers=5 \ 84 | --set controllers.module.workers=5 \ 85 | --set controllers.project.workers=5 \ 86 | --set controllers.workspace.workers=5 87 | 88 | - name: Run end-to-end test suite 89 | run: make test 90 | env: 91 | TFC_OAUTH_TOKEN: ${{ secrets.TFC_OAUTH_TOKEN }} 92 | TFC_ORG: ${{ secrets.TFC_ORG }} 93 | TFC_TOKEN: ${{ secrets.TFC_TOKEN }} 94 | TFC_VCS_REPO: ${{ secrets.TFC_VCS_REPO }} 95 | -------------------------------------------------------------------------------- /.github/workflows/helm-end-to-end-tfe.yaml: -------------------------------------------------------------------------------- 1 | name: E2E on Terraform Enterprise [Helm] 2 | 3 | on: 4 | schedule: 5 | - cron: '30 6 * * 0' 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - 'internal/controller/**' 11 | - 'charts/hcp-terraform-operator/**' 12 | workflow_dispatch: 13 | workflow_call: 14 | 15 | env: 16 | USE_EXISTING_CLUSTER: true 17 | CLUSTER_NAME: 'this' 18 | DOCKER_IMAGE: 'this' 19 | KUBECONFIG: ${{ github.workspace }}/.kube/config 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 33 | with: 34 | go-version-file: 'go.mod' 35 | 36 | - name: Set up kind 37 | uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 38 | with: 39 | wait: 2m 40 | version: v${{ vars.KIND_VERSION }} 41 | cluster_name: ${{ env.CLUSTER_NAME }} 42 | 43 | - name: Set up Helm 44 | uses: Azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 45 | with: 46 | version: v3.11.2 47 | 48 | - name: Generate Docker image metadata 49 | id: meta 50 | uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 51 | with: 52 | images: ${{ env.DOCKER_IMAGE }} 53 | tags: | 54 | type=sha,prefix=,format=short 55 | 56 | - name: Set up QEMU 57 | uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 58 | with: 59 | platforms: amd64 60 | 61 | - name: Set up Docker Buildx 62 | uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 63 | 64 | - name: Build and load Docker image 65 | uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 66 | with: 67 | platforms: linux/amd64 68 | load: true 69 | build-args: 70 | BIN_NAME=${{ vars.BIN_NAME }} 71 | tags: ${{ env.DOCKER_METADATA_OUTPUT_TAGS }} 72 | 73 | - name: Upload Docker image to kind 74 | run: | 75 | kind load docker-image ${{ env.DOCKER_METADATA_OUTPUT_TAGS }} --name ${{ env.CLUSTER_NAME }} 76 | 77 | - name: Install Helm chart 78 | run: | 79 | helm install --wait --timeout 1m this ./charts/hcp-terraform-operator \ 80 | --set operator.image.repository=${{ env.DOCKER_IMAGE }} \ 81 | --set operator.image.tag=${{ env.DOCKER_METADATA_OUTPUT_VERSION }} \ 82 | --set operator.skipTLSVerify=true \ 83 | --set operator.tfeAddress=${{ secrets.TFE_ADDRESS }} \ 84 | --set operator.syncPeriod=30s \ 85 | --set controllers.agentPool.workers=5 \ 86 | --set controllers.module.workers=5 \ 87 | --set controllers.project.workers=5 \ 88 | --set controllers.workspace.workers=5 89 | 90 | - name: Run end-to-end test suite 91 | run: make test 92 | env: 93 | TFC_OAUTH_TOKEN: ${{ secrets.TFE_OAUTH_TOKEN }} 94 | TFC_ORG: ${{ secrets.TFE_ORG }} 95 | TFC_TOKEN: ${{ secrets.TFE_TOKEN }} 96 | TFC_VCS_REPO: ${{ secrets.TFE_VCS_REPO }} 97 | TFC_TLS_SKIP_VERIFY: true 98 | TFE_ADDRESS: ${{ secrets.TFE_ADDRESS }} 99 | -------------------------------------------------------------------------------- /.github/workflows/markdown-link-check.yaml: -------------------------------------------------------------------------------- 1 | name: Check Markdown links 2 | 3 | on: 4 | schedule: 5 | - cron: '30 4 * * *' 6 | pull_request: 7 | branches: 8 | - main 9 | push: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | markdown-link-check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Run Markdown links checker 24 | uses: gaurav-nelson/github-action-markdown-link-check@d53a906aa6b22b8979d33bc86170567e619495ec # 1.0.15 25 | with: 26 | use-quiet-mode: yes 27 | use-verbose-mode: yes 28 | folder-path: './, ./docs' 29 | max-depth: 0 30 | base-branch: main 31 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: "PR Labeler" 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | pull-requests: write 10 | issues: write 11 | 12 | jobs: 13 | triage: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 18 | 19 | - name: Label Pull Request 20 | uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 21 | with: 22 | configuration-path: .github/pr-labeler.yml 23 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 24 | sync-labels: true 25 | 26 | - name: Label the PR size 27 | uses: CodelyTV/pr-size-labeler@54ef36785e9f4cb5ecf1949cfc9b00dbb621d761 # v1.8.1 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | xs_label: 'size/XS' 31 | xs_max_size: '30' 32 | s_label: 'size/S' 33 | s_max_size: '60' 34 | m_label: 'size/M' 35 | m_max_size: '150' 36 | l_label: 'size/L' 37 | l_max_size: '300' 38 | xl_label: 'size/XL' 39 | message_if_xl: '' 40 | -------------------------------------------------------------------------------- /.github/workflows/tag-release.yaml: -------------------------------------------------------------------------------- 1 | name: Tag Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - version/VERSION 9 | 10 | env: 11 | TAG: v$(cat version/VERSION) 12 | 13 | jobs: 14 | tag_release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout 18 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 19 | 20 | - name: Create tag 21 | run: | 22 | git tag ${{ env.TAG }} 23 | git push origin ${{ env.TAG }} 24 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | schedule: 5 | - cron: '15 5 * * *' 6 | pull_request: 7 | branches: 8 | - main 9 | push: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup Go 24 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 25 | with: 26 | go-version-file: 'go.mod' 27 | 28 | - name: Run Unit tests 29 | run: | 30 | make test-api 31 | make test-internal 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Mac specific files 9 | .DS_Store 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Directories 18 | bin/ 19 | .vscode/ 20 | .terraform 21 | 22 | # Terraform state 23 | .terraform.lock.hcl 24 | terraform.tfstate* 25 | 26 | # Dependency directories (remove the comment below to include it) 27 | # vendor/ 28 | 29 | # OLM Bundle 30 | bundle.Dockerfile 31 | bundle/ 32 | -------------------------------------------------------------------------------- /.release/ci.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema = "1" 5 | 6 | project "hcp-terraform-operator" { 7 | team = "terraform" 8 | slack { 9 | notification_channel = "C051FAAHL8M" # feed-terraform-ecosystem-kubernetes-releases 10 | } 11 | github { 12 | organization = "hashicorp" 13 | repository = "hcp-terraform-operator" 14 | release_branches = [ 15 | "main", 16 | "release/**", 17 | ] 18 | } 19 | } 20 | 21 | event "merge" { 22 | // "entrypoint" to use if build is not run automatically 23 | // i.e. send "merge" complete signal to orchestrator to trigger build 24 | } 25 | 26 | event "build" { 27 | depends = ["merge"] 28 | action "build" { 29 | organization = "hashicorp" 30 | repository = "hcp-terraform-operator" 31 | workflow = "build" 32 | } 33 | 34 | notification { 35 | on = "fail" 36 | } 37 | } 38 | 39 | event "prepare" { 40 | depends = ["build"] 41 | 42 | action "prepare" { 43 | organization = "hashicorp" 44 | repository = "crt-workflows-common" 45 | workflow = "prepare" 46 | } 47 | 48 | notification { 49 | on = "fail" 50 | } 51 | } 52 | 53 | ## These are promotion and post-publish events 54 | ## they should be added to the end of the file after the verify event stanza. 55 | 56 | event "trigger-staging" { 57 | // This event is dispatched by the bob trigger-promotion command 58 | // and is required - do not delete. 59 | } 60 | 61 | event "promote-staging" { 62 | depends = ["trigger-staging"] 63 | action "promote-staging" { 64 | organization = "hashicorp" 65 | repository = "crt-workflows-common" 66 | workflow = "promote-staging" 67 | config = "release-metadata.hcl" 68 | } 69 | 70 | notification { 71 | on = "always" 72 | } 73 | } 74 | 75 | event "promote-staging-docker" { 76 | depends = ["promote-staging"] 77 | action "promote-staging-docker" { 78 | organization = "hashicorp" 79 | repository = "crt-workflows-common" 80 | workflow = "promote-staging-docker" 81 | } 82 | 83 | notification { 84 | on = "always" 85 | } 86 | } 87 | 88 | event "trigger-production" { 89 | // This event is dispatched by the bob trigger-promotion command 90 | // and is required - do not delete. 91 | } 92 | 93 | event "promote-production" { 94 | depends = ["trigger-production"] 95 | action "promote-production" { 96 | organization = "hashicorp" 97 | repository = "crt-workflows-common" 98 | workflow = "promote-production" 99 | } 100 | 101 | notification { 102 | on = "always" 103 | } 104 | } 105 | 106 | event "promote-production-docker" { 107 | depends = ["promote-production"] 108 | action "promote-production-docker" { 109 | organization = "hashicorp" 110 | repository = "crt-workflows-common" 111 | workflow = "promote-production-docker" 112 | } 113 | 114 | notification { 115 | on = "always" 116 | } 117 | } 118 | 119 | event "promote-production-helm" { 120 | depends = ["promote-production-docker"] 121 | action "promote-production-helm" { 122 | organization = "hashicorp" 123 | repository = "crt-workflows-common" 124 | workflow = "promote-production-helm" 125 | } 126 | 127 | notification { 128 | on = "always" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_docker_registry_dockerhub = "https://hub.docker.com/r/hashicorp/hcp-terraform-operator" 5 | url_source_repository = "https://github.com/hashicorp/hcp-terraform-operator" 6 | url_project_website = "https://github.com/hashicorp/hcp-terraform-operator" 7 | url_license = "https://github.com/hashicorp/hcp-terraform-operator/blob/main/LICENSE" 8 | url_release_notes = "https://github.com/hashicorp/hcp-terraform-operator/blob/main/CHANGELOG.md" 9 | -------------------------------------------------------------------------------- /.release/security-scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | container { 5 | dependencies = true 6 | alpine_secdb = true 7 | secrets = true 8 | } 9 | 10 | binary { 11 | secrets = true 12 | go_modules = true 13 | osv = true 14 | oss_index = false 15 | nvd = false 16 | } 17 | -------------------------------------------------------------------------------- /.release/terraform-cloud-operator-artifacts.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema = 1 5 | artifacts { 6 | zip = [ 7 | "hcp-terraform-operator_${version}_linux_amd64.zip", 8 | "hcp-terraform-operator_${version}_linux_arm64.zip", 9 | ] 10 | container = [ 11 | "hcp-terraform-operator_release-default_linux_amd64_${version}_${commit_sha}.docker.dev.tar", 12 | "hcp-terraform-operator_release-default_linux_amd64_${version}_${commit_sha}.docker.tar", 13 | "hcp-terraform-operator_release-default_linux_arm64_${version}_${commit_sha}.docker.dev.tar", 14 | "hcp-terraform-operator_release-default_linux_arm64_${version}_${commit_sha}.docker.tar", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # This Dockerfile contains multiple targets. 5 | # Use 'docker build --target= .' to build one. 6 | # 7 | # Every target has a BIN_NAME argument that must be provided via --build-arg=BIN_NAME= 8 | # when building. 9 | 10 | ARG GO_VERSION=1.24.3 11 | 12 | # =================================== 13 | # 14 | # Non-release images. 15 | # 16 | # =================================== 17 | 18 | 19 | # dev-builder compiles the binary 20 | # ----------------------------------- 21 | FROM golang:$GO_VERSION as dev-builder 22 | 23 | ARG BIN_NAME 24 | ARG TARGETOS 25 | ARG TARGETARCH 26 | 27 | ENV BIN_NAME=$BIN_NAME 28 | 29 | WORKDIR /build 30 | 31 | COPY go.mod go.mod 32 | COPY go.sum go.sum 33 | 34 | RUN go mod download 35 | 36 | COPY api/ api/ 37 | COPY cmd/main.go cmd/main.go 38 | COPY internal/ internal/ 39 | COPY version/ version/ 40 | 41 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -trimpath -o $BIN_NAME cmd/main.go 42 | 43 | # dev runs the binary from devbuild 44 | # ----------------------------------- 45 | FROM gcr.io/distroless/static:nonroot as dev 46 | 47 | ARG BIN_NAME 48 | ARG PRODUCT_VERSION 49 | 50 | ENV BIN_NAME=$BIN_NAME 51 | 52 | LABEL version=$PRODUCT_VERSION 53 | 54 | WORKDIR / 55 | COPY --from=dev-builder /build/$BIN_NAME . 56 | USER 65532:65532 57 | 58 | ENTRYPOINT ["/bin/sh", "-c", "/$BIN_NAME"] 59 | 60 | # =================================== 61 | # 62 | # Release images. 63 | # 64 | # =================================== 65 | 66 | 67 | # default release image 68 | # ----------------------------------- 69 | FROM gcr.io/distroless/static:nonroot AS release-default 70 | 71 | ARG BIN_NAME 72 | ARG PRODUCT_VERSION 73 | ARG PRODUCT_REVISION 74 | ARG TARGETOS 75 | ARG TARGETARCH 76 | 77 | ENV BIN_NAME=$BIN_NAME 78 | 79 | LABEL maintainer="Terraform Ecosystem - Hybrid Cloud Team " 80 | LABEL version=$PRODUCT_VERSION 81 | LABEL revision=$PRODUCT_REVISION 82 | 83 | WORKDIR / 84 | COPY LICENSE /licenses/copyright.txt 85 | COPY dist/$TARGETOS/$TARGETARCH/$BIN_NAME . 86 | 87 | USER 65532:65532 88 | 89 | ENTRYPOINT ["/bin/sh", "-c", "/$BIN_NAME"] 90 | 91 | # Red Hat UBI release image 92 | # ----------------------------------- 93 | FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5 AS release-ubi 94 | 95 | ARG BIN_NAME 96 | ARG PRODUCT_VERSION 97 | ARG PRODUCT_REVISION 98 | ARG TARGETOS 99 | ARG TARGETARCH 100 | 101 | ENV BIN_NAME=$BIN_NAME 102 | 103 | LABEL name="HCP Terraform Operator" 104 | LABEL vendor="HashiCorp" 105 | LABEL release=$PRODUCT_REVISION 106 | LABEL summary="HCP Terraform Operator for Kubernetes allows managing HCP Terraform / Terraform Enterprise resources via Kubernetes Custom Resources." 107 | LABEL description="HCP Terraform Operator for Kubernetes allows managing HCP Terraform / Terraform Enterprise resources via Kubernetes Custom Resources." 108 | 109 | LABEL maintainer="HashiCorp " 110 | LABEL version=$PRODUCT_VERSION 111 | LABEL revision=$PRODUCT_REVISION 112 | 113 | WORKDIR / 114 | COPY LICENSE /licenses/copyright.txt 115 | COPY dist/$TARGETOS/$TARGETARCH/$BIN_NAME . 116 | 117 | USER 65532:65532 118 | 119 | ENTRYPOINT ["/bin/sh", "-c", "/$BIN_NAME"] 120 | 121 | # =================================== 122 | # 123 | # Set default target to 'dev'. 124 | # 125 | # =================================== 126 | FROM dev 127 | -------------------------------------------------------------------------------- /META.d/_summary.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | 6 | schema: 1.1 7 | 8 | partition: tf-ecosystem 9 | 10 | summary: 11 | owner: team-tf-hybrid-cloud 12 | description: | 13 | The HCP Terraform Operator for Kubernetes allows managing HCP Terraform / Terraform Enterprise resources via Kubernetes Custom Resources. 14 | 15 | visibility: external 16 | -------------------------------------------------------------------------------- /META.d/links.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | runbooks: [] 5 | #- name: 6 | # link: 7 | 8 | other_links: [] 9 | #- name: 10 | # link: -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: terraform.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: hcp-terraform-operator 12 | repo: github.com/hashicorp/hcp-terraform-operator 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: terraform.io 19 | group: app 20 | kind: Workspace 21 | path: github.com/hashicorp/hcp-terraform-operator/api/v1alpha2 22 | version: v1alpha2 23 | - api: 24 | crdVersion: v1 25 | namespaced: true 26 | controller: true 27 | domain: terraform.io 28 | group: app 29 | kind: Module 30 | path: github.com/hashicorp/hcp-terraform-operator/api/v1alpha2 31 | version: v1alpha2 32 | - api: 33 | crdVersion: v1 34 | namespaced: true 35 | controller: true 36 | domain: terraform.io 37 | group: app 38 | kind: AgentPool 39 | path: github.com/hashicorp/hcp-terraform-operator/api/v1alpha2 40 | version: v1alpha2 41 | - api: 42 | crdVersion: v1 43 | namespaced: true 44 | controller: true 45 | domain: terraform.io 46 | group: app 47 | kind: Project 48 | path: github.com/hashicorp/hcp-terraform-operator/api/v1alpha2 49 | version: v1alpha2 50 | version: "3" 51 | -------------------------------------------------------------------------------- /api/v1alpha2/agentpool_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | func (ap *AgentPool) IsCreationCandidate() bool { 7 | return ap.Status.AgentPoolID == "" 8 | } 9 | -------------------------------------------------------------------------------- /api/v1alpha2/agentpool_validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "fmt" 8 | 9 | apierrors "k8s.io/apimachinery/pkg/api/errors" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/util/validation/field" 12 | ) 13 | 14 | func (ap *AgentPool) ValidateSpec() error { 15 | var allErrs field.ErrorList 16 | 17 | allErrs = append(allErrs, ap.validateSpecAgentToken()...) 18 | 19 | // Validate labels 20 | if ap.Spec.AgentDeployment != nil && ap.Spec.AgentDeployment.Labels != nil { 21 | allErrs = append(allErrs, validateLabels(ap.Spec.AgentDeployment.Labels, field.NewPath("spec").Child("agentDeployment").Child("labels"))...) 22 | } 23 | 24 | // Validate annotations 25 | if ap.Spec.AgentDeployment != nil && ap.Spec.AgentDeployment.Annotations != nil { 26 | allErrs = append(allErrs, validateAnnotations(ap.Spec.AgentDeployment.Annotations, field.NewPath("spec").Child("agentDeployment").Child("annotations"))...) 27 | } 28 | 29 | if len(allErrs) == 0 { 30 | return nil 31 | } 32 | 33 | return apierrors.NewInvalid( 34 | schema.GroupKind{Group: "", Kind: "AgentPool"}, 35 | ap.Name, 36 | allErrs, 37 | ) 38 | } 39 | 40 | func (ap *AgentPool) validateSpecAgentToken() field.ErrorList { 41 | allErrs := field.ErrorList{} 42 | atn := make(map[string]int) 43 | 44 | for i, at := range ap.Spec.AgentTokens { 45 | f := field.NewPath("spec").Child(fmt.Sprintf("agentTokens[%d]", i)) 46 | 47 | if at.ID != "" { 48 | allErrs = append(allErrs, field.Forbidden( 49 | f.Child("id"), 50 | "id is not allowed in the spec"), 51 | ) 52 | } 53 | if at.CreatedAt != nil { 54 | allErrs = append(allErrs, field.Forbidden( 55 | f.Child("createdAt"), 56 | "createdAt is not allowed in the spec"), 57 | ) 58 | } 59 | if at.LastUsedAt != nil { 60 | allErrs = append(allErrs, field.Forbidden( 61 | f.Child("lastUsedAt"), 62 | "lastUsedAt is not allowed in the spec"), 63 | ) 64 | } 65 | 66 | if _, ok := atn[at.Name]; ok { 67 | allErrs = append(allErrs, field.Duplicate(f.Child("name"), at.Name)) 68 | } 69 | atn[at.Name] = i 70 | } 71 | 72 | return allErrs 73 | } 74 | 75 | // TODO:Validation 76 | // 77 | // + Invalid CR cannot be deleted until it is fixed -- need to discuss if we want to do something about it 78 | -------------------------------------------------------------------------------- /api/v1alpha2/agentpool_validation_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/hcp-terraform-operator/internal/pointer" 10 | ) 11 | 12 | func TestValidateAgentPoolSpecAgentToken(t *testing.T) { 13 | t.Parallel() 14 | 15 | successCases := map[string]AgentPool{ 16 | "HasOnlyName": { 17 | Spec: AgentPoolSpec{ 18 | AgentTokens: []*AgentToken{ 19 | { 20 | Name: "this", 21 | }, 22 | }, 23 | }, 24 | }, 25 | "HasMultipleTokens": { 26 | Spec: AgentPoolSpec{ 27 | AgentTokens: []*AgentToken{ 28 | { 29 | Name: "this", 30 | }, 31 | { 32 | Name: "self", 33 | }, 34 | }, 35 | }, 36 | }, 37 | } 38 | 39 | for n, c := range successCases { 40 | t.Run(n, func(t *testing.T) { 41 | if errs := c.validateSpecAgentToken(); len(errs) != 0 { 42 | t.Errorf("Unexpected validation errors: %v", errs) 43 | } 44 | }) 45 | } 46 | 47 | errorCases := map[string]AgentPool{ 48 | "HasID": { 49 | Spec: AgentPoolSpec{ 50 | AgentTokens: []*AgentToken{ 51 | { 52 | Name: "this", 53 | ID: "this", 54 | }, 55 | }, 56 | }, 57 | }, 58 | "HasCreatedAt": { 59 | Spec: AgentPoolSpec{ 60 | AgentTokens: []*AgentToken{ 61 | { 62 | Name: "this", 63 | CreatedAt: pointer.PointerOf(int64(1984)), 64 | }, 65 | }, 66 | }, 67 | }, 68 | "HasLastUsedAt": { 69 | Spec: AgentPoolSpec{ 70 | AgentTokens: []*AgentToken{ 71 | { 72 | Name: "this", 73 | LastUsedAt: pointer.PointerOf(int64(1984)), 74 | }, 75 | }, 76 | }, 77 | }, 78 | "HasDuplicateName": { 79 | Spec: AgentPoolSpec{ 80 | AgentTokens: []*AgentToken{ 81 | { 82 | Name: "this", 83 | }, 84 | { 85 | Name: "this", 86 | }, 87 | }, 88 | }, 89 | }, 90 | } 91 | 92 | for n, c := range errorCases { 93 | t.Run(n, func(t *testing.T) { 94 | if errs := c.validateSpecAgentToken(); len(errs) == 0 { 95 | t.Error("Unexpected failure, at least one error is expected") 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /api/v1alpha2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package v1alpha2 contains API Schema definitions for the app v1alpha2 API group 5 | // +kubebuilder:object:generate=true 6 | // +groupName=app.terraform.io 7 | package v1alpha2 8 | 9 | import ( 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "sigs.k8s.io/controller-runtime/pkg/scheme" 12 | ) 13 | 14 | var ( 15 | // GroupVersion is group version used to register these objects 16 | GroupVersion = schema.GroupVersion{Group: "app.terraform.io", Version: "v1alpha2"} 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | -------------------------------------------------------------------------------- /api/v1alpha2/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | tfc "github.com/hashicorp/go-tfe" 8 | ) 9 | 10 | func (rs *RunStatus) RunCompleted() bool { 11 | return runCompleted(rs.Status) 12 | } 13 | 14 | func (rs *PlanStatus) RunCompleted() bool { 15 | return runCompleted(rs.Status) 16 | } 17 | 18 | func runCompleted(status string) bool { 19 | // The following Run statuses indicate the completion 20 | switch status { 21 | case string(tfc.RunApplied): 22 | return true 23 | case string(tfc.RunPlannedAndFinished): 24 | return true 25 | case string(tfc.RunErrored): 26 | return true 27 | case string(tfc.RunCanceled): 28 | return true 29 | case string(tfc.RunDiscarded): 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | 36 | func (rs *RunStatus) RunApplied() bool { 37 | return runApplied(rs.Status) 38 | } 39 | 40 | // runApplied returns true if the run is applied 41 | func runApplied(status string) bool { 42 | // The following Run statuses indicate the completion 43 | switch status { 44 | case string(tfc.RunApplied): 45 | return true 46 | case string(tfc.RunPlannedAndFinished): 47 | return true 48 | } 49 | 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /api/v1alpha2/helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "testing" 8 | 9 | tfc "github.com/hashicorp/go-tfe" 10 | ) 11 | 12 | var runStatuses = map[tfc.RunStatus]struct{}{ 13 | tfc.RunApplied: {}, 14 | tfc.RunApplying: {}, 15 | tfc.RunApplyQueued: {}, 16 | tfc.RunCanceled: {}, 17 | tfc.RunConfirmed: {}, 18 | tfc.RunCostEstimated: {}, 19 | tfc.RunCostEstimating: {}, 20 | tfc.RunDiscarded: {}, 21 | tfc.RunErrored: {}, 22 | tfc.RunFetching: {}, 23 | tfc.RunFetchingCompleted: {}, 24 | tfc.RunPending: {}, 25 | tfc.RunPlanned: {}, 26 | tfc.RunPlannedAndFinished: {}, 27 | tfc.RunPlannedAndSaved: {}, 28 | tfc.RunPlanning: {}, 29 | tfc.RunPlanQueued: {}, 30 | tfc.RunPolicyChecked: {}, 31 | tfc.RunPolicyChecking: {}, 32 | tfc.RunPolicyOverride: {}, 33 | tfc.RunPolicySoftFailed: {}, 34 | tfc.RunPostPlanAwaitingDecision: {}, 35 | tfc.RunPostPlanCompleted: {}, 36 | tfc.RunPostPlanRunning: {}, 37 | tfc.RunPreApplyRunning: {}, 38 | tfc.RunPreApplyCompleted: {}, 39 | tfc.RunPrePlanCompleted: {}, 40 | tfc.RunPrePlanRunning: {}, 41 | tfc.RunQueuing: {}, 42 | tfc.RunQueuingApply: {}, 43 | } 44 | 45 | func TestRunCompleted(t *testing.T) { 46 | t.Parallel() 47 | 48 | trueCases := map[tfc.RunStatus]struct{}{ 49 | tfc.RunApplied: {}, 50 | tfc.RunCanceled: {}, 51 | tfc.RunDiscarded: {}, 52 | tfc.RunErrored: {}, 53 | tfc.RunPlannedAndFinished: {}, 54 | } 55 | 56 | for n := range runStatuses { 57 | t.Run(string(n), func(t *testing.T) { 58 | if runCompleted(string(n)) { 59 | if _, ok := trueCases[n]; !ok { 60 | t.Fatalf("Expected result to be false but got true for status %#v", n) 61 | } 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestRunApplied(t *testing.T) { 68 | t.Parallel() 69 | 70 | trueCases := map[tfc.RunStatus]struct{}{ 71 | tfc.RunApplied: {}, 72 | tfc.RunPlannedAndFinished: {}, 73 | } 74 | 75 | for n := range runStatuses { 76 | t.Run(string(n), func(t *testing.T) { 77 | if runApplied(string(n)) { 78 | if _, ok := trueCases[n]; !ok { 79 | t.Fatalf("Expected result to be false but got true for status %#v", n) 80 | } 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /api/v1alpha2/module_validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | apierrors "k8s.io/apimachinery/pkg/api/errors" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/apimachinery/pkg/util/validation/field" 10 | ) 11 | 12 | func (m *Module) ValidateSpec() error { 13 | var allErrs field.ErrorList 14 | 15 | allErrs = append(allErrs, m.validateSpecWorkspace()...) 16 | 17 | if len(allErrs) == 0 { 18 | return nil 19 | } 20 | 21 | return apierrors.NewInvalid( 22 | schema.GroupKind{Group: "", Kind: "Module"}, 23 | m.Name, 24 | allErrs, 25 | ) 26 | } 27 | 28 | func (m *Module) validateSpecWorkspace() field.ErrorList { 29 | allErrs := field.ErrorList{} 30 | spec := m.Spec.Workspace 31 | f := field.NewPath("spec").Child("workspace") 32 | 33 | if spec.ID == "" && spec.Name == "" { 34 | allErrs = append(allErrs, field.Invalid( 35 | f, 36 | "", 37 | "one of the field ID or Name must be set"), 38 | ) 39 | } 40 | 41 | if spec.ID != "" && spec.Name != "" { 42 | allErrs = append(allErrs, field.Invalid( 43 | f, 44 | "", 45 | "only one of the field ID or Name is allowed"), 46 | ) 47 | } 48 | 49 | return allErrs 50 | } 51 | 52 | // TODO:Validation 53 | // 54 | // + Variables names duplicate: spec.variables[].name 55 | // + Outputs names duplicate: spec.outputs[].name 56 | // 57 | // + Invalid CR cannot be deleted until it is fixed -- need to discuss if we want to do something about it 58 | -------------------------------------------------------------------------------- /api/v1alpha2/module_validation_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestValidateModuleSpecWorkspace(t *testing.T) { 11 | t.Parallel() 12 | 13 | successCases := map[string]Module{ 14 | "HasOnlyID": { 15 | Spec: ModuleSpec{ 16 | Workspace: &ModuleWorkspace{ 17 | ID: "this", 18 | }, 19 | }, 20 | }, 21 | "HasOnlyName": { 22 | Spec: ModuleSpec{ 23 | Workspace: &ModuleWorkspace{ 24 | Name: "this", 25 | }, 26 | }, 27 | }, 28 | } 29 | 30 | for n, c := range successCases { 31 | t.Run(n, func(t *testing.T) { 32 | if errs := c.validateSpecWorkspace(); len(errs) != 0 { 33 | t.Errorf("Unexpected validation errors: %v", errs) 34 | } 35 | }) 36 | } 37 | 38 | errorCases := map[string]Module{ 39 | "HasIDandName": { 40 | Spec: ModuleSpec{ 41 | Workspace: &ModuleWorkspace{ 42 | ID: "this", 43 | Name: "this", 44 | }, 45 | }, 46 | }, 47 | "HasEmptyIDandName": { 48 | Spec: ModuleSpec{ 49 | Workspace: &ModuleWorkspace{}, 50 | }, 51 | }, 52 | } 53 | 54 | for n, c := range errorCases { 55 | t.Run(n, func(t *testing.T) { 56 | if errs := c.validateSpecWorkspace(); len(errs) == 0 { 57 | t.Error("Unexpected failure, at least one error is expected") 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /api/v1alpha2/project_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | func (p *Project) IsCreationCandidate() bool { 7 | return p.Status.ID == "" 8 | } 9 | -------------------------------------------------------------------------------- /api/v1alpha2/project_helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | const ( 11 | projectFinalizer = "project.app.terraform.io/finalizer" 12 | ) 13 | 14 | func TestIsCreationCandidate(t *testing.T) { 15 | t.Parallel() 16 | 17 | cases := map[string]struct { 18 | project Project 19 | expected bool 20 | }{ 21 | "HasID": { 22 | Project{Status: ProjectStatus{ID: "prj-this"}}, 23 | false, 24 | }, 25 | "DoesNotHaveID": { 26 | Project{Status: ProjectStatus{ID: ""}}, 27 | true, 28 | }, 29 | } 30 | 31 | for n, c := range cases { 32 | t.Run(n, func(t *testing.T) { 33 | out := c.project.IsCreationCandidate() 34 | if out != c.expected { 35 | t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /api/v1alpha2/project_validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "fmt" 8 | 9 | tfc "github.com/hashicorp/go-tfe" 10 | apierrors "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/apimachinery/pkg/util/validation/field" 13 | ) 14 | 15 | func (p *Project) ValidateSpec() error { 16 | var allErrs field.ErrorList 17 | 18 | allErrs = append(allErrs, p.validateSpecTeamAccess()...) 19 | 20 | if len(allErrs) == 0 { 21 | return nil 22 | } 23 | 24 | return apierrors.NewInvalid( 25 | schema.GroupKind{Group: "", Kind: "Project"}, 26 | p.Name, 27 | allErrs, 28 | ) 29 | } 30 | 31 | func (p *Project) validateSpecTeamAccess() field.ErrorList { 32 | allErrs := field.ErrorList{} 33 | 34 | allErrs = append(allErrs, p.validateSpecTeamAccessCustom()...) 35 | allErrs = append(allErrs, p.validateSpecTeamAccessTeam()...) 36 | 37 | return allErrs 38 | } 39 | 40 | func (p *Project) validateSpecTeamAccessCustom() field.ErrorList { 41 | allErrs := field.ErrorList{} 42 | 43 | for i, ta := range p.Spec.TeamAccess { 44 | f := field.NewPath("spec").Child(fmt.Sprintf("[%d]", i)) 45 | if ta.Access == tfc.TeamProjectAccessCustom { 46 | if ta.Custom == nil { 47 | allErrs = append(allErrs, field.Required( 48 | f, 49 | fmt.Sprintf("'spec.teamAccess.custom' must be set when 'spec.teamAccess' is set to %q", tfc.TeamProjectAccessCustom), 50 | )) 51 | } 52 | } else { 53 | if ta.Custom != nil { 54 | allErrs = append(allErrs, field.Invalid( 55 | f, 56 | "", 57 | fmt.Sprintf("'spec.teamAccess.custom' cannot be used when 'spec.teamAccess' is set to %s", ta.Access), 58 | )) 59 | } 60 | } 61 | } 62 | 63 | return allErrs 64 | } 65 | 66 | func (p *Project) validateSpecTeamAccessTeam() field.ErrorList { 67 | allErrs := field.ErrorList{} 68 | 69 | tai := make(map[string]int) 70 | tan := make(map[string]int) 71 | 72 | for i, ta := range p.Spec.TeamAccess { 73 | f := field.NewPath("spec").Child(fmt.Sprintf("[%d]", i)) 74 | if ta.Team.ID == "" && ta.Team.Name == "" { 75 | allErrs = append(allErrs, field.Invalid( 76 | f, 77 | "", 78 | "one of the field ID or Name must be set"), 79 | ) 80 | } 81 | 82 | if ta.Team.ID != "" && ta.Team.Name != "" { 83 | allErrs = append(allErrs, field.Invalid( 84 | f, 85 | "", 86 | "only one of the field ID or Name is allowed"), 87 | ) 88 | } 89 | 90 | if ta.Team.ID != "" { 91 | if _, ok := tai[ta.Team.ID]; ok { 92 | allErrs = append(allErrs, field.Duplicate(f.Child("ID"), ta.Team.ID)) 93 | } 94 | tai[ta.Team.ID] = i 95 | } 96 | 97 | if ta.Team.Name != "" { 98 | if _, ok := tan[ta.Team.Name]; ok { 99 | allErrs = append(allErrs, field.Duplicate(f.Child("Name"), ta.Team.Name)) 100 | } 101 | tan[ta.Team.Name] = i 102 | } 103 | } 104 | 105 | return allErrs 106 | } 107 | -------------------------------------------------------------------------------- /api/v1alpha2/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/util/validation/field" 8 | ) 9 | 10 | // Validating labels to ensure key value pairs are not empty 11 | func validateLabels(labels map[string]string, fldPath *field.Path) field.ErrorList { 12 | allErrs := field.ErrorList{} 13 | for k, v := range labels { 14 | if k == "" { 15 | allErrs = append(allErrs, field.Required(fldPath.Child("key"), "key must not be empty")) 16 | } 17 | if v == "" { 18 | allErrs = append(allErrs, field.Required(fldPath.Child("value"), "value must not be empty")) 19 | } 20 | } 21 | return allErrs 22 | } 23 | 24 | // Validate annotations to ensure key value pairs are not empty 25 | func validateAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList { 26 | allErrs := field.ErrorList{} 27 | for k, v := range annotations { 28 | if k == "" { 29 | allErrs = append(allErrs, field.Required(fldPath.Child("key"), "key must not be empty")) 30 | } 31 | if v == "" { 32 | allErrs = append(allErrs, field.Required(fldPath.Child("value"), "value must not be empty")) 33 | } 34 | } 35 | return allErrs 36 | } 37 | -------------------------------------------------------------------------------- /api/v1alpha2/validation_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "testing" 8 | 9 | "k8s.io/apimachinery/pkg/util/validation/field" 10 | ) 11 | 12 | func TestValidateLabels(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | labels map[string]string 16 | wantErr bool 17 | }{ 18 | { 19 | name: "HasValidLabel", 20 | labels: map[string]string{ 21 | "key": "my-value", 22 | }, 23 | wantErr: false, 24 | }, 25 | { 26 | name: "HasEmptyKey", 27 | labels: map[string]string{ 28 | "": "my-value", 29 | }, 30 | wantErr: true, 31 | }, 32 | { 33 | name: "HasEmptyValue", 34 | labels: map[string]string{ 35 | "key": "", 36 | }, 37 | wantErr: true, 38 | }, 39 | } 40 | 41 | for _, l := range tests { 42 | t.Run(l.name, func(t *testing.T) { 43 | errs := validateLabels(l.labels, field.NewPath("metadata").Child("labels")) 44 | if (len(errs) > 0) != l.wantErr { 45 | t.Errorf("validateLabels() error = %v, wantErr %v", errs, l.wantErr) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestValidateAnnotations(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | annotations map[string]string 55 | wantErr bool 56 | }{ 57 | { 58 | name: "HasValidAnnotations", 59 | annotations: map[string]string{ 60 | "key": "my-value", 61 | }, 62 | wantErr: false, 63 | }, 64 | { 65 | name: "HasEmptyKey", 66 | annotations: map[string]string{ 67 | "": "my-value", 68 | }, 69 | wantErr: true, 70 | }, 71 | { 72 | name: "HasEmptyValue", 73 | annotations: map[string]string{ 74 | "key": "", 75 | }, 76 | wantErr: true, 77 | }, 78 | } 79 | 80 | for _, a := range tests { 81 | t.Run(a.name, func(t *testing.T) { 82 | errs := validateAnnotations(a.annotations, field.NewPath("metadata").Child("annotations")) 83 | if (len(errs) > 0) != a.wantErr { 84 | t.Errorf("validateAnnotations() error = %v, wantErr %v", errs, a.wantErr) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /api/v1alpha2/workspace_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha2 5 | 6 | import ( 7 | "github.com/hashicorp/hcp-terraform-operator/internal/slice" 8 | ) 9 | 10 | func (w *Workspace) IsCreationCandidate() bool { 11 | return w.Status.WorkspaceID == "" 12 | } 13 | 14 | // AddOrUpdateVariableStatus adds a given variable to the status if it does not exist there; otherwise, it updates it. 15 | func (s *WorkspaceStatus) AddOrUpdateVariableStatus(variable VariableStatus) { 16 | for i, v := range s.Variables { 17 | if v.Name == variable.Name && v.Category == variable.Category { 18 | s.Variables[i].ID = variable.ID 19 | s.Variables[i].VersionID = variable.VersionID 20 | s.Variables[i].ValueID = variable.ValueID 21 | s.Variables[i].Category = variable.Category 22 | return 23 | } 24 | } 25 | 26 | s.Variables = append(s.Variables, VariableStatus{ 27 | Name: variable.Name, 28 | ID: variable.ID, 29 | VersionID: variable.VersionID, 30 | ValueID: variable.ValueID, 31 | Category: variable.Category, 32 | }) 33 | } 34 | 35 | // GetVariableStatus returns a given variable from the status if it exists there; otherwise, nil. 36 | func (s *WorkspaceStatus) GetVariableStatus(variable VariableStatus) *VariableStatus { 37 | for _, v := range s.Variables { 38 | if v.Name == variable.Name && v.Category == variable.Category { 39 | return &v 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // DeleteVariableStatus deletes a given variable from the status. 47 | func (s *WorkspaceStatus) DeleteVariableStatus(variable VariableStatus) { 48 | for i, v := range s.Variables { 49 | if v.Name == variable.Name && v.Category == variable.Category { 50 | s.Variables = slice.RemoveFromSlice(s.Variables, i) 51 | return 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: v2 5 | name: hcp-terraform-operator 6 | description: Official Helm chart for HCP Terraform Operator for Kubernetes. 7 | type: application 8 | version: "2.9.2" 9 | appVersion: "2.9.2" 10 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Thank you for installing HashiCorp HCP Terraform Operator! 2 | 3 | Documentation: 4 | - https://github.com/hashicorp/hcp-terraform-operator 5 | 6 | Your release is named {{ .Release.Name }}. 7 | 8 | To get the release status, run: 9 | $ helm --namespace {{ .Release.Namespace }} status {{ .Release.Name }} 10 | 11 | To get the release values, run: 12 | $ helm --namespace {{ .Release.Namespace }} get values {{ .Release.Name }} 13 | 14 | To read this notes again, run: 15 | $ helm --namespace {{ .Release.Namespace }} get notes {{ .Release.Name }} 16 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "hcp-terraform-operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "hcp-terraform-operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "hcp-terraform-operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "hcp-terraform-operator.labels" -}} 37 | helm.sh/chart: {{ include "hcp-terraform-operator.chart" . }} 38 | {{ include "hcp-terraform-operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "hcp-terraform-operator.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "hcp-terraform-operator.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "hcp-terraform-operator.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "hcp-terraform-operator.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/clusterrole_manager.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | creationTimestamp: null 9 | name: {{ .Release.Name }}-manager-role 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - configmaps 15 | - secrets 16 | verbs: 17 | - create 18 | - list 19 | - update 20 | - watch 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - events 25 | verbs: 26 | - create 27 | - patch 28 | - apiGroups: 29 | - app.terraform.io 30 | resources: 31 | - agentpools 32 | - modules 33 | - projects 34 | - workspaces 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | - apiGroups: 44 | - app.terraform.io 45 | resources: 46 | - agentpools/finalizers 47 | - modules/finalizers 48 | - projects/finalizers 49 | - workspaces/finalizers 50 | verbs: 51 | - update 52 | - apiGroups: 53 | - app.terraform.io 54 | resources: 55 | - agentpools/status 56 | - modules/status 57 | - projects/status 58 | - workspaces/status 59 | verbs: 60 | - get 61 | - patch 62 | - update 63 | - apiGroups: 64 | - apps 65 | resources: 66 | - deployments 67 | verbs: 68 | - create 69 | - delete 70 | - get 71 | - list 72 | - patch 73 | - update 74 | - watch 75 | {{- end -}} 76 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/clusterrole_metrics_reader.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | name: {{ .Release.Name }}-metrics-reader 9 | rules: 10 | - nonResourceURLs: 11 | - /metrics 12 | verbs: 13 | - get 14 | {{- end -}} 15 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/clusterrole_proxy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | name: {{ .Release.Name }}-proxy-role 9 | rules: 10 | - apiGroups: 11 | - authentication.k8s.io 12 | resources: 13 | - tokenreviews 14 | verbs: 15 | - create 16 | - apiGroups: 17 | - authorization.k8s.io 18 | resources: 19 | - subjectaccessreviews 20 | verbs: 21 | - create 22 | {{- end -}} 23 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/clusterrolebinding_manager.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRoleBinding 7 | metadata: 8 | name: {{ .Release.Name }}-manager-rolebinding 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: {{ .Release.Name }}-manager-role 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "hcp-terraform-operator.serviceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/clusterrolebinding_proxy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRoleBinding 7 | metadata: 8 | name: {{ .Release.Name }}-proxy-rolebinding 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: {{ .Release.Name }}-proxy-role 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "hcp-terraform-operator.serviceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.customCAcertificates -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: v1 6 | kind: ConfigMap 7 | metadata: 8 | name: {{ .Release.Name }}-ca-certificates 9 | namespace: {{ .Release.Namespace }} 10 | binaryData: 11 | ca-certificates: {{ .Values.customCAcertificates }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: Role 7 | metadata: 8 | name: {{ .Release.Name }}-leader-election-role 9 | namespace: {{ .Release.Namespace }} 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - configmaps 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - create 20 | - update 21 | - patch 22 | - delete 23 | - apiGroups: 24 | - coordination.k8s.io 25 | resources: 26 | - leases 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | - create 32 | - update 33 | - patch 34 | - delete 35 | - apiGroups: 36 | - "" 37 | resources: 38 | - events 39 | verbs: 40 | - create 41 | - patch 42 | {{- end -}} 43 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: RoleBinding 7 | metadata: 8 | name: {{ .Release.Name }}-leader-election-rolebinding 9 | namespace: {{ .Release.Namespace }} 10 | roleRef: 11 | apiGroup: rbac.authorization.k8s.io 12 | kind: Role 13 | name: {{ .Release.Name }}-leader-election-role 14 | subjects: 15 | - kind: ServiceAccount 16 | name: {{ include "hcp-terraform-operator.serviceAccountName" . }} 17 | namespace: {{ .Release.Namespace }} 18 | {{- end -}} 19 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | labels: 8 | control-plane: {{ .Release.Name }}-controller-manager 9 | name: {{ .Release.Name }}-controller-manager-metrics-service 10 | namespace: {{ .Release.Namespace }} 11 | spec: 12 | ports: 13 | - name: https 14 | port: 8443 15 | protocol: TCP 16 | targetPort: https 17 | selector: 18 | control-plane: {{ .Release.Name }}-controller-manager 19 | -------------------------------------------------------------------------------- /charts/hcp-terraform-operator/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | name: {{ include "hcp-terraform-operator.serviceAccountName" . }} 9 | namespace: {{ .Release.Namespace }} 10 | labels: 11 | {{- include "hcp-terraform-operator.labels" . | nindent 4 }} 12 | {{- with .Values.serviceAccount.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /charts/test/unit/rbac_clusster_role_binding_manager_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | ) 13 | 14 | func TestRBACClusterRoleBindingManagerCreateTrue(t *testing.T) { 15 | options := &helm.Options{ 16 | SetValues: map[string]string{ 17 | "rbac.create": "true", 18 | }, 19 | Version: helmChartVersion, 20 | } 21 | rbac := renderRBACClusterRoleBindingManagerManifest(t, options) 22 | 23 | assert.Equal(t, defaultRBACClusterRoleBindingManagerName, rbac.Name) 24 | assert.Empty(t, rbac.Labels) 25 | assert.Empty(t, rbac.Annotations) 26 | 27 | testRBACClusterRoleBindingManagerRoleRef(t, rbac) 28 | testRBACClusterRoleBindingManagerSubjects(t, rbac, defaultNamespace) 29 | } 30 | 31 | func TestRBACClusterRoleBindingManagerCreateFalse(t *testing.T) { 32 | options := &helm.Options{ 33 | SetValues: map[string]string{ 34 | "rbac.create": "false", 35 | }, 36 | Version: helmChartVersion, 37 | } 38 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{rbacRoleBindingTemplate}) 39 | 40 | assert.Error(t, err) 41 | assert.Contains(t, err.Error(), "could not find template") 42 | } 43 | 44 | func testRBACClusterRoleBindingManagerRoleRef(t *testing.T, rbac rbacv1.ClusterRoleBinding) { 45 | roleRef := rbacv1.RoleRef{ 46 | APIGroup: rbacv1.GroupName, 47 | Kind: "ClusterRole", 48 | Name: defaultRBACClusterRoleManagerName, 49 | } 50 | assert.Equal(t, roleRef, rbac.RoleRef) 51 | } 52 | 53 | func testRBACClusterRoleBindingManagerSubjects(t *testing.T, rbac rbacv1.ClusterRoleBinding, ns string) { 54 | subjects := []rbacv1.Subject{ 55 | { 56 | Kind: "ServiceAccount", 57 | Name: defaultServiceAccountName, 58 | Namespace: ns, 59 | }, 60 | } 61 | assert.Equal(t, subjects, rbac.Subjects) 62 | } 63 | -------------------------------------------------------------------------------- /charts/test/unit/rbac_clusster_role_binding_proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | ) 13 | 14 | func TestRBACClusterRoleBindingProxyCreateTrue(t *testing.T) { 15 | options := &helm.Options{ 16 | SetValues: map[string]string{ 17 | "rbac.create": "true", 18 | }, 19 | Version: helmChartVersion, 20 | } 21 | rbac := renderRBACClusterRoleBindingProxyManifest(t, options) 22 | 23 | assert.Equal(t, defaultRBACClusterRoleBindingProxyName, rbac.Name) 24 | assert.Empty(t, rbac.Labels) 25 | assert.Empty(t, rbac.Annotations) 26 | 27 | testRBACClusterRoleBindingProxyRoleRef(t, rbac) 28 | testRBACClusterRoleBindingProxySubjects(t, rbac, defaultNamespace) 29 | } 30 | 31 | func TestRBACClusterRoleBindingProxyCreateFalse(t *testing.T) { 32 | options := &helm.Options{ 33 | SetValues: map[string]string{ 34 | "rbac.create": "false", 35 | }, 36 | Version: helmChartVersion, 37 | } 38 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{rbacRoleBindingTemplate}) 39 | 40 | assert.Error(t, err) 41 | assert.Contains(t, err.Error(), "could not find template") 42 | } 43 | 44 | func testRBACClusterRoleBindingProxyRoleRef(t *testing.T, rbac rbacv1.ClusterRoleBinding) { 45 | roleRef := rbacv1.RoleRef{ 46 | APIGroup: rbacv1.GroupName, 47 | Kind: "ClusterRole", 48 | Name: defaultRBACClusterRoleProxyName, 49 | } 50 | assert.Equal(t, roleRef, rbac.RoleRef) 51 | } 52 | 53 | func testRBACClusterRoleBindingProxySubjects(t *testing.T, rbac rbacv1.ClusterRoleBinding, ns string) { 54 | subjects := []rbacv1.Subject{ 55 | { 56 | Kind: "ServiceAccount", 57 | Name: defaultServiceAccountName, 58 | Namespace: ns, 59 | }, 60 | } 61 | assert.Equal(t, subjects, rbac.Subjects) 62 | } 63 | -------------------------------------------------------------------------------- /charts/test/unit/rbac_clusster_role_manager_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | ) 13 | 14 | func TestRBACClusterRoleManagerCreateTrue(t *testing.T) { 15 | options := &helm.Options{ 16 | SetValues: map[string]string{ 17 | "rbac.create": "true", 18 | }, 19 | Version: helmChartVersion, 20 | } 21 | rbac := renderRBACClusterRoleManagerManifest(t, options) 22 | 23 | assert.Equal(t, defaultRBACClusterRoleManagerName, rbac.Name) 24 | assert.Empty(t, rbac.Labels) 25 | assert.Empty(t, rbac.Annotations) 26 | 27 | testRBACClusterRoleManagerRules(t, rbac) 28 | } 29 | 30 | func TestRBAClusterRoleManagerCreateFalse(t *testing.T) { 31 | options := &helm.Options{ 32 | SetValues: map[string]string{ 33 | "rbac.create": "false", 34 | }, 35 | Version: helmChartVersion, 36 | } 37 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{rbacClusterRoleManagerTemplate}) 38 | 39 | assert.Error(t, err) 40 | assert.Contains(t, err.Error(), "could not find template") 41 | } 42 | 43 | func testRBACClusterRoleManagerRules(t *testing.T, rbac rbacv1.ClusterRole) { 44 | rules := []rbacv1.PolicyRule{ 45 | { 46 | Verbs: []string{ 47 | "create", 48 | "list", 49 | "update", 50 | "watch", 51 | }, 52 | APIGroups: []string{""}, 53 | Resources: []string{ 54 | "configmaps", 55 | "secrets", 56 | }, 57 | }, 58 | { 59 | Verbs: []string{ 60 | "create", 61 | "patch", 62 | }, 63 | APIGroups: []string{""}, 64 | Resources: []string{"events"}, 65 | }, 66 | { 67 | Verbs: []string{ 68 | "create", 69 | "delete", 70 | "get", 71 | "list", 72 | "patch", 73 | "update", 74 | "watch", 75 | }, 76 | APIGroups: []string{"app.terraform.io"}, 77 | Resources: []string{ 78 | "agentpools", 79 | "modules", 80 | "projects", 81 | "workspaces", 82 | }, 83 | }, 84 | { 85 | Verbs: []string{ 86 | "update", 87 | }, 88 | APIGroups: []string{"app.terraform.io"}, 89 | Resources: []string{ 90 | "agentpools/finalizers", 91 | "modules/finalizers", 92 | "projects/finalizers", 93 | "workspaces/finalizers", 94 | }, 95 | }, 96 | { 97 | Verbs: []string{ 98 | "get", 99 | "patch", 100 | "update", 101 | }, 102 | APIGroups: []string{"app.terraform.io"}, 103 | Resources: []string{ 104 | "agentpools/status", 105 | "modules/status", 106 | "projects/status", 107 | "workspaces/status", 108 | }, 109 | }, 110 | { 111 | Verbs: []string{ 112 | "create", 113 | "delete", 114 | "get", 115 | "list", 116 | "patch", 117 | "update", 118 | "watch", 119 | }, 120 | APIGroups: []string{"apps"}, 121 | Resources: []string{"deployments"}, 122 | }, 123 | } 124 | assert.Equal(t, rules, rbac.Rules) 125 | } 126 | -------------------------------------------------------------------------------- /charts/test/unit/rbac_clusster_role_metrics_reader_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | ) 13 | 14 | func TestRBACClusterRoleMetricsReaderCreateTrue(t *testing.T) { 15 | options := &helm.Options{ 16 | SetValues: map[string]string{ 17 | "rbac.create": "true", 18 | }, 19 | Version: helmChartVersion, 20 | } 21 | rbac := renderRBACClusterRoleMetricsReaderManifest(t, options) 22 | 23 | assert.Equal(t, defaultRBACClusterRoleMetricsReaderName, rbac.Name) 24 | assert.Empty(t, rbac.Labels) 25 | assert.Empty(t, rbac.Annotations) 26 | 27 | testRBACClusterRoleMetricsReaderRules(t, rbac) 28 | } 29 | 30 | func TestRBACClusterRoleMetricsReaderCreateFalse(t *testing.T) { 31 | options := &helm.Options{ 32 | SetValues: map[string]string{ 33 | "rbac.create": "false", 34 | }, 35 | Version: helmChartVersion, 36 | } 37 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{rbacClusterRoleMetricsReaderTemplate}) 38 | 39 | assert.Error(t, err) 40 | assert.Contains(t, err.Error(), "could not find template") 41 | } 42 | 43 | func testRBACClusterRoleMetricsReaderRules(t *testing.T, rbac rbacv1.ClusterRole) { 44 | rules := []rbacv1.PolicyRule{ 45 | { 46 | NonResourceURLs: []string{"/metrics"}, 47 | Verbs: []string{"get"}, 48 | }, 49 | } 50 | assert.Equal(t, rules, rbac.Rules) 51 | } 52 | -------------------------------------------------------------------------------- /charts/test/unit/rbac_clusster_role_proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | ) 13 | 14 | func TestRBACClusterRoleProxyCreateTrue(t *testing.T) { 15 | options := &helm.Options{ 16 | SetValues: map[string]string{ 17 | "rbac.create": "true", 18 | }, 19 | Version: helmChartVersion, 20 | } 21 | rbac := renderRBACClusterRoleProxyManifest(t, options) 22 | 23 | assert.Equal(t, defaultRBACClusterRoleProxyName, rbac.Name) 24 | assert.Empty(t, rbac.Labels) 25 | assert.Empty(t, rbac.Annotations) 26 | 27 | testRBACClusterRoleProxyRules(t, rbac) 28 | } 29 | 30 | func TestRBACClusterRoleProxyCreateFalse(t *testing.T) { 31 | options := &helm.Options{ 32 | SetValues: map[string]string{ 33 | "rbac.create": "false", 34 | }, 35 | Version: helmChartVersion, 36 | } 37 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{rbacClusterRoleProxyTemplate}) 38 | 39 | assert.Error(t, err) 40 | assert.Contains(t, err.Error(), "could not find template") 41 | } 42 | 43 | func testRBACClusterRoleProxyRules(t *testing.T, rbac rbacv1.ClusterRole) { 44 | rules := []rbacv1.PolicyRule{ 45 | { 46 | APIGroups: []string{"authentication.k8s.io"}, 47 | Resources: []string{"tokenreviews"}, 48 | Verbs: []string{"create"}, 49 | }, 50 | { 51 | APIGroups: []string{"authorization.k8s.io"}, 52 | Resources: []string{"subjectaccessreviews"}, 53 | Verbs: []string{"create"}, 54 | }, 55 | } 56 | assert.Equal(t, rules, rbac.Rules) 57 | } 58 | -------------------------------------------------------------------------------- /charts/test/unit/rbac_role_binding_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | ) 13 | 14 | func TestRBACRoleBindingCreateTrue(t *testing.T) { 15 | options := &helm.Options{ 16 | SetValues: map[string]string{ 17 | "rbac.create": "true", 18 | }, 19 | Version: helmChartVersion, 20 | } 21 | rbac := renderRBACRoleBindingManifest(t, options) 22 | 23 | assert.Equal(t, defaultRBACRoleBindingName, rbac.Name) 24 | assert.Empty(t, rbac.Labels) 25 | assert.Empty(t, rbac.Annotations) 26 | assert.Equal(t, defaultNamespace, rbac.Namespace) 27 | 28 | testRBACRoleBindingRoleRef(t, rbac) 29 | testRBACRoleBindingSubjects(t, rbac, defaultNamespace) 30 | } 31 | 32 | func TestRBACRoleBindingCreateFalse(t *testing.T) { 33 | options := &helm.Options{ 34 | SetValues: map[string]string{ 35 | "rbac.create": "false", 36 | }, 37 | Version: helmChartVersion, 38 | } 39 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{rbacRoleBindingTemplate}) 40 | 41 | assert.Error(t, err) 42 | assert.Contains(t, err.Error(), "could not find template") 43 | } 44 | 45 | func TestRBACRoleBindingNamespace(t *testing.T) { 46 | ns := "this" 47 | options := &helm.Options{ 48 | SetValues: map[string]string{ 49 | "rbac.create": "true", 50 | }, 51 | EnvVars: map[string]string{ 52 | "HELM_NAMESPACE": ns, 53 | }, 54 | Version: helmChartVersion, 55 | } 56 | rbac := renderRBACRoleBindingManifest(t, options) 57 | 58 | assert.Equal(t, defaultRBACRoleBindingName, rbac.Name) 59 | assert.Empty(t, rbac.Labels) 60 | assert.Empty(t, rbac.Annotations) 61 | assert.Equal(t, ns, rbac.Namespace) 62 | 63 | testRBACRoleBindingRoleRef(t, rbac) 64 | testRBACRoleBindingSubjects(t, rbac, ns) 65 | } 66 | 67 | func testRBACRoleBindingRoleRef(t *testing.T, rbac rbacv1.RoleBinding) { 68 | roleRef := rbacv1.RoleRef{ 69 | APIGroup: rbacv1.GroupName, 70 | Kind: "Role", 71 | Name: defaultRBACRoleName, 72 | } 73 | assert.Equal(t, roleRef, rbac.RoleRef) 74 | } 75 | 76 | func testRBACRoleBindingSubjects(t *testing.T, rbac rbacv1.RoleBinding, ns string) { 77 | subjects := []rbacv1.Subject{ 78 | { 79 | Kind: "ServiceAccount", 80 | Name: defaultServiceAccountName, 81 | Namespace: ns, 82 | }, 83 | } 84 | assert.Equal(t, subjects, rbac.Subjects) 85 | } 86 | -------------------------------------------------------------------------------- /charts/test/unit/rbac_role_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | ) 13 | 14 | func TestRBACRoleCreateTrue(t *testing.T) { 15 | options := &helm.Options{ 16 | SetValues: map[string]string{ 17 | "rbac.create": "true", 18 | }, 19 | Version: helmChartVersion, 20 | } 21 | rbac := renderRBACRoleManifest(t, options) 22 | 23 | assert.Equal(t, defaultRBACRoleName, rbac.Name) 24 | assert.Empty(t, rbac.Labels) 25 | assert.Empty(t, rbac.Annotations) 26 | assert.Equal(t, defaultNamespace, rbac.Namespace) 27 | 28 | testRBACRoleRules(t, rbac) 29 | } 30 | 31 | func TestRBACRoleCreateFalse(t *testing.T) { 32 | options := &helm.Options{ 33 | SetValues: map[string]string{ 34 | "rbac.create": "false", 35 | }, 36 | Version: helmChartVersion, 37 | } 38 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{rbacRoleTemplate}) 39 | 40 | assert.Error(t, err) 41 | assert.Contains(t, err.Error(), "could not find template") 42 | } 43 | 44 | func TestRBACRoleNamespace(t *testing.T) { 45 | ns := "this" 46 | options := &helm.Options{ 47 | SetValues: map[string]string{ 48 | "rbac.create": "true", 49 | }, 50 | EnvVars: map[string]string{ 51 | "HELM_NAMESPACE": ns, 52 | }, 53 | Version: helmChartVersion, 54 | } 55 | rbac := renderRBACRoleManifest(t, options) 56 | 57 | assert.Equal(t, defaultRBACRoleName, rbac.Name) 58 | assert.Empty(t, rbac.Labels) 59 | assert.Empty(t, rbac.Annotations) 60 | assert.Equal(t, ns, rbac.Namespace) 61 | 62 | testRBACRoleRules(t, rbac) 63 | } 64 | 65 | func testRBACRoleRules(t *testing.T, rbac rbacv1.Role) { 66 | rules := []rbacv1.PolicyRule{ 67 | { 68 | Verbs: []string{ 69 | "get", 70 | "list", 71 | "watch", 72 | "create", 73 | "update", 74 | "patch", 75 | "delete", 76 | }, 77 | APIGroups: []string{""}, 78 | Resources: []string{"configmaps"}, 79 | }, 80 | { 81 | Verbs: []string{ 82 | "get", 83 | "list", 84 | "watch", 85 | "create", 86 | "update", 87 | "patch", 88 | "delete", 89 | }, 90 | APIGroups: []string{"coordination.k8s.io"}, 91 | Resources: []string{"leases"}, 92 | }, 93 | { 94 | Verbs: []string{ 95 | "create", 96 | "patch", 97 | }, 98 | APIGroups: []string{""}, 99 | Resources: []string{"events"}, 100 | }, 101 | } 102 | assert.Equal(t, rules, rbac.Rules) 103 | } 104 | -------------------------------------------------------------------------------- /charts/test/unit/service_account_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unit 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gruntwork-io/terratest/modules/helm" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestServiceAccountCreateTrue(t *testing.T) { 14 | options := &helm.Options{ 15 | SetValues: map[string]string{ 16 | "serviceAccount.create": "true", 17 | }, 18 | Version: helmChartVersion, 19 | } 20 | sa := renderServiceAccountManifest(t, options) 21 | 22 | assert.Equal(t, defaultServiceAccountName, sa.Name) 23 | assert.Equal(t, defaultServiceAccountLabels, sa.Labels) 24 | assert.Empty(t, sa.Annotations) 25 | assert.Equal(t, defaultNamespace, sa.Namespace) 26 | } 27 | 28 | func TestServiceAccountCreateFalse(t *testing.T) { 29 | options := &helm.Options{ 30 | SetValues: map[string]string{ 31 | "serviceAccount.create": "false", 32 | }, 33 | Version: helmChartVersion, 34 | } 35 | _, err := helm.RenderTemplateE(t, options, helmChartPath, helmReleaseName, []string{serviceAccountTemplate}) 36 | 37 | assert.Error(t, err) 38 | assert.Contains(t, err.Error(), "could not find template") 39 | } 40 | 41 | func TestServiceAccountAnnotations(t *testing.T) { 42 | expectedAnnotations := map[string]string{ 43 | "app.kubernetes.io/name": "hcp-terraform-operator", 44 | } 45 | options := &helm.Options{ 46 | SetValues: map[string]string{ 47 | "serviceAccount.create": "true", 48 | }, 49 | SetJsonValues: map[string]string{ 50 | "serviceAccount.annotations": `{"app.kubernetes.io/name": "hcp-terraform-operator"}`, 51 | }, 52 | Version: helmChartVersion, 53 | } 54 | sa := renderServiceAccountManifest(t, options) 55 | 56 | assert.Equal(t, defaultServiceAccountName, sa.Name) 57 | assert.Equal(t, defaultServiceAccountLabels, sa.Labels) 58 | assert.Equal(t, expectedAnnotations, sa.Annotations) 59 | assert.Equal(t, defaultNamespace, sa.Namespace) 60 | } 61 | 62 | func TestServiceAccountNamespace(t *testing.T) { 63 | ns := "this" 64 | options := &helm.Options{ 65 | SetValues: map[string]string{ 66 | "serviceAccount.create": "true", 67 | }, 68 | EnvVars: map[string]string{ 69 | "HELM_NAMESPACE": ns, 70 | }, 71 | Version: helmChartVersion, 72 | } 73 | sa := renderServiceAccountManifest(t, options) 74 | 75 | assert.Equal(t, defaultServiceAccountName, sa.Name) 76 | assert.Equal(t, defaultServiceAccountLabels, sa.Labels) 77 | assert.Empty(t, sa.Annotations) 78 | assert.Equal(t, ns, sa.Namespace) 79 | } 80 | 81 | func TestServiceAccountName(t *testing.T) { 82 | name := "this" 83 | options := &helm.Options{ 84 | SetValues: map[string]string{ 85 | "serviceAccount.create": "true", 86 | "serviceAccount.name": name, 87 | }, 88 | Version: helmChartVersion, 89 | } 90 | sa := renderServiceAccountManifest(t, options) 91 | 92 | assert.Equal(t, name, sa.Name) 93 | assert.Equal(t, defaultServiceAccountLabels, sa.Labels) 94 | assert.Empty(t, sa.Annotations) 95 | assert.Equal(t, defaultNamespace, sa.Namespace) 96 | } 97 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/app.terraform.io_workspaces.yaml 6 | - bases/app.terraform.io_modules.yaml 7 | - bases/app.terraform.io_agentpools.yaml 8 | - bases/app.terraform.io_projects.yaml 9 | #+kubebuilder:scaffold:crdkustomizeresource 10 | 11 | patches: 12 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 13 | # patches here are for enabling the conversion webhook for each CRD 14 | #- patches/webhook_in_workspaces.yaml 15 | #- patches/webhook_in_modules.yaml 16 | #- patches/webhook_in_agentpools.yaml 17 | #- patches/webhook_in_projects.yaml 18 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 19 | 20 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 21 | # patches here are for enabling the CA injection for each CRD 22 | #- patches/cainjection_in_workspaces.yaml 23 | #- patches/cainjection_in_modules.yaml 24 | #- patches/cainjection_in_agentpools.yaml 25 | #- patches/cainjection_in_projects.yaml 26 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 27 | 28 | # [WEBHOOK] To enable webhook, uncomment the following section 29 | # the following config is for teaching kustomize how to do kustomization for CRDs. 30 | 31 | #configurations: 32 | #- kustomizeconfig.yaml 33 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_agentpools.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: agentpools.app.terraform.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_modules.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: modules.app.terraform.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_projects.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: projects.app.terraform.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_workspaces.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: workspaces.app.terraform.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_agentpools.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: agentpools.app.terraform.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_modules.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: modules.app.terraform.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_projects.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: projects.app.terraform.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_workspaces.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: workspaces.app.terraform.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: registry.redhat.io/openshift4/ose-kube-rbac-proxy:v4.15 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: registry.connect.redhat.com/hashicorp/hcp-terraform-operator 8 | newTag: 2.9.2 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: hcp-terraform-operator 7 | name: system 8 | --- 9 | apiVersion: apps/v1 10 | kind: Deployment 11 | metadata: 12 | name: controller-manager 13 | namespace: system 14 | labels: 15 | control-plane: controller-manager 16 | app.kubernetes.io/name: hcp-terraform-operator 17 | spec: 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | replicas: 1 22 | template: 23 | metadata: 24 | annotations: 25 | kubectl.kubernetes.io/default-container: manager 26 | labels: 27 | control-plane: controller-manager 28 | spec: 29 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 30 | # according to the platforms which are supported by your solution. 31 | # It is considered best practice to support multiple architectures. You can 32 | # build your manager image using the makefile target docker-buildx. 33 | # affinity: 34 | # nodeAffinity: 35 | # requiredDuringSchedulingIgnoredDuringExecution: 36 | # nodeSelectorTerms: 37 | # - matchExpressions: 38 | # - key: kubernetes.io/arch 39 | # operator: In 40 | # values: 41 | # - amd64 42 | # - arm64 43 | # - ppc64le 44 | # - s390x 45 | # - key: kubernetes.io/os 46 | # operator: In 47 | # values: 48 | # - linux 49 | securityContext: 50 | runAsNonRoot: true 51 | # TODO(user): For common cases that do not require escalating privileges 52 | # it is recommended to ensure that all your Pods/Containers are restrictive. 53 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 54 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 55 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 56 | # seccompProfile: 57 | # type: RuntimeDefault 58 | containers: 59 | - command: 60 | - /manager 61 | args: 62 | - --sync-period=5m 63 | - --agent-pool-workers=1 64 | - --agent-pool-sync-period=30s 65 | - --module-workers=1 66 | - --module-sync-period=5m 67 | - --project-workers=1 68 | - --project-sync-period=5m 69 | - --workspace-workers=1 70 | - --workspace-sync-period=5m 71 | image: controller:latest 72 | name: manager 73 | securityContext: 74 | allowPrivilegeEscalation: false 75 | capabilities: 76 | drop: 77 | - ALL 78 | seccompProfile: 79 | type: RuntimeDefault 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 50m 100 | memory: 64Mi 101 | serviceAccountName: hcp-terraform-operator-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/hcp-terraform-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patches: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | 24 | # path: /spec/template/spec/containers/0/volumeMounts/0 25 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 26 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 27 | # - op: remove 28 | # path: /spec/template/spec/volumes/0 29 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: hcp-terraform-operator 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /config/rbac/agentpool_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit agentpools. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: agentpool-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: hcp-terraform-operator 10 | app.kubernetes.io/part-of: hcp-terraform-operator 11 | name: agentpool-editor-role 12 | rules: 13 | - apiGroups: 14 | - app.terraform.io 15 | resources: 16 | - agentpools 17 | verbs: 18 | - create 19 | - delete 20 | - get 21 | - list 22 | - patch 23 | - update 24 | - watch 25 | - apiGroups: 26 | - app.terraform.io 27 | resources: 28 | - agentpools/status 29 | verbs: 30 | - get 31 | -------------------------------------------------------------------------------- /config/rbac/agentpool_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view agentpools. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: agentpool-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: hcp-terraform-operator 10 | app.kubernetes.io/part-of: hcp-terraform-operator 11 | name: agentpool-viewer-role 12 | rules: 13 | - apiGroups: 14 | - app.terraform.io 15 | resources: 16 | - agentpools 17 | verbs: 18 | - get 19 | - list 20 | - watch 21 | - apiGroups: 22 | - app.terraform.io 23 | resources: 24 | - agentpools/status 25 | verbs: 26 | - get 27 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: hcp-terraform-operator 6 | name: metrics-reader 7 | rules: 8 | - nonResourceURLs: 9 | - "/metrics" 10 | verbs: 11 | - get 12 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: hcp-terraform-operator 6 | name: proxy-rolebinding 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: proxy-role 11 | subjects: 12 | - kind: ServiceAccount 13 | name: controller-manager 14 | namespace: system 15 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: hcp-terraform-operator 7 | name: controller-manager-metrics-service 8 | namespace: system 9 | spec: 10 | ports: 11 | - name: https 12 | port: 8443 13 | protocol: TCP 14 | targetPort: https 15 | selector: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | # - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | # - auth_proxy_service.yaml 16 | # - auth_proxy_role.yaml 17 | # - auth_proxy_role_binding.yaml 18 | # - auth_proxy_client_clusterrole.yaml 19 | # For each CRD, "Editor" and "Viewer" roles are scaffolded by 20 | # default, aiding admins in cluster management. Those roles are 21 | # not used by the Project itself. You can comment the following lines 22 | # if you do not want those helpers be installed with your Project. 23 | # - agentpool_editor_role.yaml 24 | # - agentpool_viewer_role.yaml 25 | # - module_editor_role.yaml 26 | # - module_viewer_role.yaml 27 | # - project_editor_role.yaml 28 | # - project_viewer_role.yaml 29 | # - workspace_editor_role.yaml 30 | # - workspace_viewer_role.yaml 31 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: hcp-terraform-operator 7 | name: leader-election 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - create 18 | - update 19 | - patch 20 | - delete 21 | - apiGroups: 22 | - coordination.k8s.io 23 | resources: 24 | - leases 25 | verbs: 26 | - get 27 | - list 28 | - watch 29 | - create 30 | - update 31 | - patch 32 | - delete 33 | - apiGroups: 34 | - "" 35 | resources: 36 | - events 37 | verbs: 38 | - create 39 | - patch 40 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: hcp-terraform-operator 6 | name: leader-election 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: Role 10 | name: hcp-terraform-operator-leader-election 11 | subjects: 12 | - kind: ServiceAccount 13 | name: hcp-terraform-operator-manager 14 | namespace: system 15 | -------------------------------------------------------------------------------- /config/rbac/module_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit modules. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: module-editor-role 6 | rules: 7 | - apiGroups: 8 | - app.terraform.io 9 | resources: 10 | - modules 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - app.terraform.io 21 | resources: 22 | - modules/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/module_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view modules. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: module-viewer-role 6 | rules: 7 | - apiGroups: 8 | - app.terraform.io 9 | resources: 10 | - modules 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - app.terraform.io 17 | resources: 18 | - modules/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/project_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit projects. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: project-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: hcp-terraform-operator 10 | app.kubernetes.io/part-of: hcp-terraform-operator 11 | name: project-editor-role 12 | rules: 13 | - apiGroups: 14 | - app.terraform.io 15 | resources: 16 | - projects 17 | verbs: 18 | - create 19 | - delete 20 | - get 21 | - list 22 | - patch 23 | - update 24 | - watch 25 | - apiGroups: 26 | - app.terraform.io 27 | resources: 28 | - projects/status 29 | verbs: 30 | - get 31 | -------------------------------------------------------------------------------- /config/rbac/project_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view projects. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: project-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: hcp-terraform-operator 10 | app.kubernetes.io/part-of: hcp-terraform-operator 11 | name: project-viewer-role 12 | rules: 13 | - apiGroups: 14 | - app.terraform.io 15 | resources: 16 | - projects 17 | verbs: 18 | - get 19 | - list 20 | - watch 21 | - apiGroups: 22 | - app.terraform.io 23 | resources: 24 | - projects/status 25 | verbs: 26 | - get 27 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: hcp-terraform-operator 7 | name: manager 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps 13 | - secrets 14 | verbs: 15 | - create 16 | - list 17 | - update 18 | - watch 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - events 23 | verbs: 24 | - create 25 | - patch 26 | - apiGroups: 27 | - app.terraform.io 28 | resources: 29 | - agentpools 30 | - modules 31 | - projects 32 | - workspaces 33 | verbs: 34 | - create 35 | - delete 36 | - get 37 | - list 38 | - patch 39 | - update 40 | - watch 41 | - apiGroups: 42 | - app.terraform.io 43 | resources: 44 | - agentpools/finalizers 45 | - modules/finalizers 46 | - projects/finalizers 47 | - workspaces/finalizers 48 | verbs: 49 | - update 50 | - apiGroups: 51 | - app.terraform.io 52 | resources: 53 | - agentpools/status 54 | - modules/status 55 | - projects/status 56 | - workspaces/status 57 | verbs: 58 | - get 59 | - patch 60 | - update 61 | - apiGroups: 62 | - apps 63 | resources: 64 | - deployments 65 | verbs: 66 | - create 67 | - delete 68 | - get 69 | - list 70 | - patch 71 | - update 72 | - watch 73 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: hcp-terraform-operator 6 | name: manager 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: hcp-terraform-operator-manager 11 | subjects: 12 | - kind: ServiceAccount 13 | name: hcp-terraform-operator-manager 14 | namespace: system 15 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: hcp-terraform-operator 6 | name: manager 7 | namespace: system 8 | -------------------------------------------------------------------------------- /config/rbac/workspace_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit workspaces. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: workspace-editor-role 6 | rules: 7 | - apiGroups: 8 | - app.terraform.io 9 | resources: 10 | - workspaces 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - app.terraform.io 21 | resources: 22 | - workspaces/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/workspace_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view workspaces. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: workspace-viewer-role 6 | rules: 7 | - apiGroups: 8 | - app.terraform.io 9 | resources: 10 | - workspaces 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - app.terraform.io 17 | resources: 18 | - workspaces/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/samples/app_v1alpha2_agentpool.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.terraform.io/v1alpha2 2 | kind: AgentPool 3 | metadata: 4 | name: NAME 5 | spec: 6 | organization: HCP_TF_ORG_NAME 7 | token: 8 | secretKeyRef: 9 | name: SECRET_NAME 10 | key: SECRET_KEY 11 | name: NAME 12 | agentTokens: 13 | - name: token 14 | -------------------------------------------------------------------------------- /config/samples/app_v1alpha2_module.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.terraform.io/v1alpha2 2 | kind: Module 3 | metadata: 4 | name: NAME 5 | spec: 6 | organization: HCP_TF_ORG_NAME 7 | token: 8 | secretKeyRef: 9 | name: SECRET_NAME 10 | key: SECRET_KEY 11 | module: 12 | source: redeux/terraform-cloud-agent/kubernetes 13 | version: 1.0.1 14 | workspace: 15 | id: WORKSPACE_ID 16 | -------------------------------------------------------------------------------- /config/samples/app_v1alpha2_project.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.terraform.io/v1alpha2 2 | kind: Project 3 | metadata: 4 | name: NAME 5 | spec: 6 | organization: HCP_TF_ORG_NAME 7 | token: 8 | secretKeyRef: 9 | name: SECRET_NAME 10 | key: SECRET_KEY 11 | name: NAME 12 | -------------------------------------------------------------------------------- /config/samples/app_v1alpha2_workspace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: app.terraform.io/v1alpha2 2 | kind: Workspace 3 | metadata: 4 | name: NAME 5 | spec: 6 | organization: HCP_TF_ORG_NAME 7 | token: 8 | secretKeyRef: 9 | name: SECRET_NAME 10 | key: SECRET_KEY 11 | name: NAME 12 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - app_v1alpha2_workspace.yaml 4 | - app_v1alpha2_module.yaml 5 | - app_v1alpha2_agentpool.yaml 6 | - app_v1alpha2_project.yaml 7 | #+kubebuilder:scaffold:manifestskustomizesamples 8 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patches: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patches 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.37.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.37.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.37.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.37.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.37.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.37.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /docs/annotations-and-labels.md: -------------------------------------------------------------------------------- 1 | # Annotations and Labels used by HCP Terraform Operator 2 | 3 | ## Annotations 4 | 5 | | Annotation key | Target resources | Possible values | Description | 6 | | --- | --- | --- | --- | 7 | | `workspace.app.terraform.io/run-new` | Workspace | `"true"` | Set this annotation to `"true"` to trigger a new run. Example: `kubectl annotate workspace workspace.app.terraform.io/run-new="true"`. | 8 | | `workspace.app.terraform.io/run-type` | Workspace | `plan`, `apply`, `refresh` | Specifies the run type. Changing this annotation does not start a new run. Refer to [Run Modes and Options](https://developer.hashicorp.com/terraform/cloud-docs/run/modes-and-options) for more information. Defaults to `"plan"`. | 9 | | `workspace.app.terraform.io/run-terraform-version` | Workspace | Any valid Terraform version | Specifies the Terraform version to use. Changing this annotation does not start a new run. Only valid when the annotation `workspace.app.terraform.io/run-type` is set to `plan`. Defaults to the Workspace version. | 10 | 11 | ## Labels 12 | 13 | | Label key | Target resources | Possible values | Description | 14 | | --- | --- | --- | --- | 15 | | `app.terraform.io/crd-schema-version` | CRD[All] | A valid calendar versioning tag format: `vYY.MM.PATCH`. | The label is used to version the HCP Operator CRD. The version is updated whenever there is a change in the schema, following the [calendar versioning](https://calver.org/) approach. | 16 | | `agentpool.app.terraform.io/pool-name` | Pod[Agent] | Any valid AgentPool name | Associate the resource with a specific agent pool by specifying the name of the agent pool. | 17 | | `agentpool.app.terraform.io/pool-id` | Pod[Agent] | Any valid AgentPool ID | Associate the resource with a specific agent pool by specifying the ID of the agent pool. | 18 | -------------------------------------------------------------------------------- /docs/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Configuration file for CRD Reference Documentation Generator: https://github.com/elastic/crd-ref-docs 5 | processor: 6 | ignoreTypes: 7 | - "AgentPoolList$" 8 | - "ModuleList$" 9 | - "ProjectList$" 10 | - "WorkspaceList$" 11 | ignoreFields: 12 | - "status$" 13 | render: 14 | kubernetesVersion: 1.27 15 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | [Here](./) you can find different examples of the YAML manifests. They aim to demonstrate the usage of the different features supported by the Operator. These examples are not comprehensive, and do not illustrate all possible configurations. 4 | 5 | All examples must be based on basic examples `*-basic.yaml`. 6 | 7 | If you encounter any issues with examples or want to add more, please feel free to open a new issue! 8 | -------------------------------------------------------------------------------- /docs/examples/agentPool-basic.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: AgentPool 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: agent-pool-demo 16 | agentTokens: 17 | - name: token 18 | -------------------------------------------------------------------------------- /docs/examples/agentPool-deployment.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: AgentPool 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: agent-pool-demo 16 | agentTokens: 17 | - name: token 18 | agentDeployment: 19 | replicas: 3 20 | spec: 21 | containers: 22 | - name: tfc-agent 23 | image: "hashicorp/tfc-agent" 24 | -------------------------------------------------------------------------------- /docs/examples/module-basic.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Module 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | module: 16 | source: redeux/terraform-cloud-agent/kubernetes 17 | version: 1.0.1 18 | workspace: 19 | id: ws-NUVHA9feCXzAmPHx 20 | # Alternatively, you can use the Workspace name: 21 | # 22 | # name: workspace-name 23 | -------------------------------------------------------------------------------- /docs/examples/module-moduleName.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Module 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: this 16 | module: 17 | source: redeux/terraform-cloud-agent/kubernetes 18 | version: 1.0.1 19 | workspace: 20 | id: ws-NUVHA9feCXzAmPHx 21 | # Alternatively, you can use the Workspace name: 22 | # 23 | # name: workspace-name 24 | -------------------------------------------------------------------------------- /docs/examples/module-variables-and-outputs.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Module 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | module: 16 | source: redeux/terraform-cloud-agent/kubernetes 17 | version: 1.0.1 18 | workspace: 19 | id: ws-NUVHA9feCXzAmPHx 20 | # Alternatively, you can use the Workspace name: 21 | # 22 | # name: workspace-name 23 | variables: 24 | - name: variable_a 25 | - name: variable_b 26 | outputs: 27 | - name: output_a 28 | - name: output_b 29 | -------------------------------------------------------------------------------- /docs/examples/project-basic.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Project 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: project-demo 16 | -------------------------------------------------------------------------------- /docs/examples/project-customTeamAccess.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Project 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: project-demo 16 | teamAccess: 17 | - team: 18 | name: demo 19 | access: custom 20 | custom: 21 | projectAccess: read 22 | teamManagement: read 23 | createWorkspace: false 24 | deleteWorkspace: false 25 | moveWorkspace: false 26 | lockWorkspace: false 27 | runs: read 28 | runTasks: false 29 | sentinelMocks: read 30 | stateVersions: read-outputs 31 | variables: read 32 | -------------------------------------------------------------------------------- /docs/examples/project-teamAccess.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Project 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: project-demo 16 | teamAccess: 17 | - team: 18 | name: demo 19 | access: admin 20 | -------------------------------------------------------------------------------- /docs/examples/workspace-basic.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | applyMethod: manual 17 | applyRunTrigger: manual 18 | -------------------------------------------------------------------------------- /docs/examples/workspace-basicNotifications.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | notifications: 17 | - name: SRESlack 18 | type: slack 19 | url: https://hooks.slack.com/ 20 | triggers: 21 | - run:needs_attention 22 | - assessment:check_failure 23 | - assessment:drifted 24 | - assessment:failed 25 | - name: SREEmail 26 | type: email 27 | emailUsers: 28 | - sre@example.com 29 | triggers: 30 | - run:completed 31 | - name: SRETeams 32 | type: microsoft-teams 33 | enabled: false 34 | url: https://example.webhook.office.com/ 35 | triggers: 36 | - assessment:check_failure 37 | - assessment:drifted 38 | - assessment:failed 39 | - run:applying 40 | - run:completed 41 | - run:created 42 | - run:errored 43 | - run:needs_attention 44 | - run:planning 45 | - name: SRELambda 46 | type: generic 47 | enabled: false 48 | url: https://lambda.eu-central-1.amazonaws.com/ 49 | token: t0k3n 50 | triggers: 51 | - run:planning 52 | -------------------------------------------------------------------------------- /docs/examples/workspace-project.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | project: 17 | name: kubernetes-operator 18 | # Alternatively, you can use the Project ID: 19 | # 20 | # id: prj-e89jyCXbxi1sU2AR 21 | -------------------------------------------------------------------------------- /docs/examples/workspace-runTasks.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | runTasks: 17 | - id: task-ovqauvpxHF4AqTru 18 | stage: pre_plan 19 | - name: run-task-demo 20 | enforcementLevel: mandatory 21 | -------------------------------------------------------------------------------- /docs/examples/workspace-runTriggers.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | runTriggers: 17 | - id: ws-NUVHA9feCXzAmPHx 18 | - name: target-workspace 19 | -------------------------------------------------------------------------------- /docs/examples/workspace-terraformVariables.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | # More about Terraform variables: https://developer.hashicorp.com/terraform/language/values/variables#assigning-values-to-root-module-variables 17 | # More about HCP Terraform Workspace Variables: https://developer.hashicorp.com/terraform/cloud-docs/workspaces/variables 18 | terraformVariables: 19 | - name: counter 20 | hcl: true 21 | value: > 22 | [ 23 | 1, 24 | 2, 25 | 4, 26 | 8, 27 | 16, 28 | 32 29 | ] 30 | -------------------------------------------------------------------------------- /docs/examples/workspace-terraformVersion.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | terraformVersion: 1.3.7 17 | -------------------------------------------------------------------------------- /docs/examples/workspace-variable-sets.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: app.terraform.io/v1alpha2 6 | kind: Workspace 7 | metadata: 8 | name: this 9 | spec: 10 | organization: kubernetes-operator 11 | token: 12 | secretKeyRef: 13 | name: tfc-operator 14 | key: token 15 | name: kubernetes-operator-demo 16 | variableSets: 17 | - id: "varset-hLdht7LL9mdV4MYD" 18 | -------------------------------------------------------------------------------- /docs/metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | The Operator exposes metrics in the [Prometheus](https://prometheus.io/) format for each controller. They are available at the standard `/metrics` path over the HTTPS port `8443`. 4 | 5 | The metrics are protected by [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy). This allows providing RBAC-based access to the metrics within the Kubernetes cluster. 6 | 7 | ## Available Metrics 8 | 9 | The Operator exposes all metrics provided by the controller-runtime by default. The full list you can find on the [Kubebuilder documentation](https://book.kubebuilder.io/reference/metrics-reference.html). 10 | 11 | ## Scraping Metrics 12 | 13 | How metrics are scraped will depend on how you operate your Prometheus server. The below example assumes that the [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator) is being used to run Prometheus. 14 | 15 | If the Operator is deployed with helm, a Kubernetes Cluster IP service resource is created. This service should be used as a target for Prometheus. The service name builds by the following template: `{{ .Release.Name }}-controller-manager-metrics-service` 16 | 17 | Below is an example of the Prometheus Operator ConfigMap to scrape metrics from the Operator Helm release named `tfc-operator`: 18 | 19 | ```yaml 20 | apiVersion: v1 21 | data: 22 | ... 23 | prometheus.yml: | 24 | ... 25 | - job_name: tfc-operator 26 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 27 | scheme: https 28 | scrape_interval: 1m 29 | scrape_timeout: 10s 30 | static_configs: 31 | - targets: 32 | - tfc-operator-controller-manager-metrics-service:8443 33 | tls_config: 34 | insecure_skip_verify: true 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/module.md: -------------------------------------------------------------------------------- 1 | # `Module` 2 | 3 | `Module` controller allows executing arbitrary Terraform Modules code in HCP Terraform Workspace via Kubernetes Custom Resources. 4 | 5 | Please refer to the [CRD](../config/crd/bases/app.terraform.io_modules.yaml) and [API Reference](./api-reference.md#module) to get the full list of available options. 6 | 7 | Below is an example of a Module Custom Resource: 8 | 9 | ```yaml 10 | apiVersion: app.terraform.io/v1alpha2 11 | kind: Module 12 | metadata: 13 | name: this 14 | spec: 15 | organization: kubernetes-operator 16 | token: 17 | secretKeyRef: 18 | name: tfc-operator 19 | key: token 20 | destroyOnDeletion: true 21 | module: 22 | source: app.terraform.io/kubernetes-operator/module-random/provider 23 | version: 0.0.5 24 | variables: 25 | - name: counter 26 | outputs: 27 | - name: secret 28 | sensitive: true 29 | - name: random_strings 30 | workspace: 31 | name: kubernetes-operator-demo 32 | ``` 33 | 34 | The above CR will be transformed to the following terraform code and then executed within the `kubernetes-operator-demo` workspace: 35 | 36 | ```hcl 37 | variable "counter" {} 38 | 39 | module "this" { 40 | source = "app.terraform.io/kubernetes-operator/module-random/provider" 41 | version = "0.0.5" 42 | 43 | counter = var.counter 44 | } 45 | 46 | output "secret" { 47 | value = module.this.secret 48 | sensitive = true 49 | } 50 | 51 | output "random_strings" { 52 | value = module.this.random_strings 53 | } 54 | ``` 55 | 56 | Non-sensitive outputs will be saved in Kubernetes ConfigMaps. Sensitive outputs will be saved in Kubernetes Secrets. In both cases, the name of the corresponding Kubernetes object will be generated automatically and has the following pattern: `-module-outputs`. For the above example, the name of ConfigMap and Secret will be `this-module-outputs`. 57 | 58 | Please note that the `Module` controller does not create a workspace or variables in the referred workspace. They must exist. 59 | 60 | In order to restart reconciliation for a particular CR, execute the following command: 61 | 62 | ```console 63 | $ kubectl patch module \ 64 | --type=merge \ 65 | --patch '{"spec": {"restartedAt": "'`date -u -Iseconds`'"}}' 66 | ``` 67 | 68 | If you have any questions, please check out the [FAQ](./faq.md#module-controller). 69 | 70 | If you encounter any issues with the `Module` controller please refer to the [Troubleshooting](../README.md#troubleshooting). 71 | -------------------------------------------------------------------------------- /docs/project.md: -------------------------------------------------------------------------------- 1 | # `Project` 2 | 3 | `Project` controller allows managing HCP Terraform Projects via Kubernetes Custom Resources. 4 | 5 | Please refer to the [CRD](../config/crd/bases/app.terraform.io_projects.yaml) and [API Reference](./api-reference.md#project) to get the full list of available options. 6 | 7 | Below is a basic example of a Project Custom Resource: 8 | 9 | ```yaml 10 | apiVersion: app.terraform.io/v1alpha2 11 | kind: Project 12 | metadata: 13 | name: this 14 | spec: 15 | organization: kubernetes-operator 16 | token: 17 | secretKeyRef: 18 | name: tfc-operator 19 | key: token 20 | name: project-demo 21 | ``` 22 | 23 | Once the above CR is applied, the Operator creates a new project `project-demo` under the `kubernetes-operator` organization. 24 | 25 | The example can be extended with team access permission support: 26 | 27 | ```yaml 28 | apiVersion: app.terraform.io/v1alpha2 29 | kind: Project 30 | metadata: 31 | name: this 32 | spec: 33 | organization: kubernetes-operator 34 | token: 35 | secretKeyRef: 36 | name: tfc-operator 37 | key: token 38 | name: project-demo 39 | teamAccess: 40 | - team: 41 | name: demo 42 | access: admin 43 | ``` 44 | 45 | The team `demo` will get `Admin` [permission group](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/permissions) access to workspaces under the project `project-demo`. 46 | 47 | If you have any questions, please check out the [FAQ](./faq.md#project-controller). 48 | 49 | If you encounter any issues with the `Project` controller please refer to the [Troubleshooting](../README.md#troubleshooting). 50 | -------------------------------------------------------------------------------- /docs/templates/markdown/gv_details.tpl: -------------------------------------------------------------------------------- 1 | {{- define "gvDetails" -}} 2 | {{- $gv := . -}} 3 | 4 | ## {{ $gv.GroupVersionString }} 5 | 6 | {{ $gv.Doc }} 7 | 8 | {{- if $gv.Kinds }} 9 | ### Resource Types 10 | {{- range $gv.SortedKinds }} 11 | - {{ $gv.TypeForKind . | markdownRenderTypeLink }} 12 | {{- end }} 13 | {{ end }} 14 | 15 | {{ range $gv.SortedTypes }} 16 | {{ template "type" . }} 17 | {{ end }} 18 | 19 | {{- end -}} 20 | -------------------------------------------------------------------------------- /docs/templates/markdown/gv_list.tpl: -------------------------------------------------------------------------------- 1 | {{- define "gvList" -}} 2 | {{- $groupVersions := . -}} 3 | 4 | # API Reference 5 | 6 | ## Packages 7 | {{- range $groupVersions }} 8 | - {{ markdownRenderGVLink . }} 9 | {{- end }} 10 | 11 | {{ range $groupVersions }} 12 | {{ template "gvDetails" . }} 13 | {{ end }} 14 | 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /docs/templates/markdown/type.tpl: -------------------------------------------------------------------------------- 1 | {{- define "type" -}} 2 | {{- $type := . -}} 3 | {{- if markdownShouldRenderType $type -}} 4 | 5 | #### {{ $type.Name }} 6 | 7 | {{ if $type.IsAlias }}_Underlying type:_ _{{ markdownRenderTypeLink $type.UnderlyingType }}_{{ end }} 8 | 9 | {{ $type.Doc }} 10 | 11 | {{ if $type.References -}} 12 | _Appears in:_ 13 | {{- range $type.SortedReferences }} 14 | - {{ markdownRenderTypeLink . }} 15 | {{- end }} 16 | {{- end }} 17 | 18 | {{ if $type.Members -}} 19 | | Field | Description | 20 | | --- | --- | 21 | {{ if $type.GVK -}} 22 | | `apiVersion` _string_ | `{{ $type.GVK.Group }}/{{ $type.GVK.Version }}` 23 | | `kind` _string_ | `{{ $type.GVK.Kind }}` 24 | {{ end -}} 25 | 26 | {{ range $type.Members -}} 27 | | `{{ .Name }}` _{{ markdownRenderType .Type }}_ | {{ template "type_members" . }} | 28 | {{ end -}} 29 | 30 | {{ end -}} 31 | 32 | {{- end -}} 33 | {{- end -}} 34 | -------------------------------------------------------------------------------- /docs/templates/markdown/type_members.tpl: -------------------------------------------------------------------------------- 1 | {{- define "type_members" -}} 2 | {{- $field := . -}} 3 | {{- if eq $field.Name "metadata" -}} 4 | Refer to Kubernetes API documentation for fields of `metadata`. 5 | {{- else -}} 6 | {{ markdownRenderFieldDoc $field.Doc }} 7 | {{- end -}} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Prerequisites 4 | 5 | - The Operator requires a HCP Terraform [organization](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/organizations) name and a [team 'owners' token](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens#team-api-tokens) in order to access the HCP Terraform API. 6 | - The API token must be stored in a Kubernetes secret. 7 | - A single instance of the Operator can manage HCP Terraform resources for different organizations and/or different API tokens. For that purpose, the organization name and a reference to the corresponding Kubernetes secret are shipped within the custom resource. 8 | 9 | Below are examples of how to create a Kubernetes secret and store the API token there. The examples assume that the API token is already known. 10 | 11 | 1. `kubectl` command 12 | 13 | ```console 14 | $ kubectl create secret generic tfc-operator --from-literal=token=APIt0k3n 15 | ``` 16 | 17 | 2. YAML manifest 18 | - Encode the API token 19 | 20 | ```console 21 | $ echo -n "APIt0k3n" | base64 22 | ``` 23 | 24 | - Create a YAML manifest and paste the encoded token from the previous step 25 | 26 | ```yaml 27 | apiVersion: v1 28 | kind: Secret 29 | metadata: 30 | name: tfc-operator 31 | type: Opaque 32 | data: 33 | token: QVBJdDBrM24= 34 | ``` 35 | 36 | - Apply YAML manifest 37 | 38 | ```console 39 | $ kubectl apply -f secret.yaml 40 | ``` 41 | 42 | For more information about Kubernetes secrets please refer to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/secret/). Please use the approach that is matching with the best practices which are accepted in your organization. 43 | 44 | Controllers usage guides: 45 | - [AgentPool](../docs/agentpool.md) 46 | - [Module](../docs/module.md) 47 | - [Workspace](../docs/workspace.md) 48 | -------------------------------------------------------------------------------- /docs/workspace.md: -------------------------------------------------------------------------------- 1 | # `Workspace` 2 | 3 | `Workspace` controller allows managing Terraform Cloud Workspaces via Kubernetes Custom Resources. 4 | 5 | Please refer to the [CRD](../config/crd/bases/app.terraform.io_workspaces.yaml) and [API Reference](./api-reference.md#workspace) to get the full list of available options. 6 | 7 | Below is an example of a Workspace Custom Resource: 8 | 9 | ```yaml 10 | apiVersion: app.terraform.io/v1alpha2 11 | kind: Workspace 12 | metadata: 13 | name: this 14 | spec: 15 | organization: kubernetes-operator 16 | token: 17 | secretKeyRef: 18 | name: tfc-operator 19 | key: token 20 | name: workspace-demo 21 | description: Kubernetes Operator Automated Workspace 22 | applyMethod: auto 23 | terraformVersion: 1.3.2 24 | executionMode: remote 25 | terraformVariables: 26 | - name: counter 27 | hcl: true 28 | value: > 29 | [ 30 | 1, 31 | 2, 32 | 4, 33 | 8, 34 | 16, 35 | 32 36 | ] 37 | tags: 38 | - demo 39 | ``` 40 | 41 | Once the above CR is applied, the Operator creates a new workspace `workspace-demo` under the `kubernetes-operator` organization. 42 | 43 | Non-sensitive outputs of the workspace runs will be saved in Kubernetes ConfigMaps. Sensitive outputs of the workspace runs will be saved in Kubernetes Secrets. In both cases, the name of the corresponding Kubernetes object will be generated automatically and has the following pattern: `-outputs`. For the above example, the name of ConfigMap and Secret will be `this-outputs`. 44 | 45 | If you have any questions, please check out the [FAQ](./faq.md#workspace-controller). 46 | 47 | If you encounter any issues with the `Workspace` controller please refer to the [Troubleshooting](../README.md#troubleshooting). 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/hcp-terraform-operator 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.2 7 | github.com/go-logr/zapr v1.3.0 8 | github.com/google/go-cmp v0.7.0 9 | github.com/hashicorp/go-slug v0.16.4 10 | github.com/hashicorp/go-tfe v1.76.0 11 | github.com/onsi/ginkgo/v2 v2.23.3 12 | github.com/onsi/gomega v1.36.3 13 | go.uber.org/zap v1.27.0 14 | k8s.io/api v0.32.3 15 | k8s.io/apimachinery v0.32.3 16 | k8s.io/client-go v0.32.3 17 | sigs.k8s.io/controller-runtime v0.20.4 18 | ) 19 | 20 | require ( 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 24 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 25 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 26 | github.com/fsnotify/fsnotify v1.7.0 // indirect 27 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 28 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 29 | github.com/go-openapi/jsonreference v0.20.2 // indirect 30 | github.com/go-openapi/swag v0.23.0 // indirect 31 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/btree v1.1.3 // indirect 35 | github.com/google/gnostic-models v0.6.8 // indirect 36 | github.com/google/go-querystring v1.1.0 // indirect 37 | github.com/google/gofuzz v1.2.0 // indirect 38 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 39 | github.com/google/uuid v1.6.0 // indirect 40 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 41 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 42 | github.com/hashicorp/go-version v1.7.0 // indirect 43 | github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e // indirect 44 | github.com/josharian/intern v1.0.0 // indirect 45 | github.com/json-iterator/go v1.1.12 // indirect 46 | github.com/mailru/easyjson v0.7.7 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 50 | github.com/pkg/errors v0.9.1 // indirect 51 | github.com/prometheus/client_golang v1.19.1 // indirect 52 | github.com/prometheus/client_model v0.6.1 // indirect 53 | github.com/prometheus/common v0.55.0 // indirect 54 | github.com/prometheus/procfs v0.15.1 // indirect 55 | github.com/spf13/pflag v1.0.5 // indirect 56 | github.com/x448/float16 v0.8.4 // indirect 57 | go.uber.org/multierr v1.11.0 // indirect 58 | golang.org/x/net v0.38.0 // indirect 59 | golang.org/x/oauth2 v0.28.0 // indirect 60 | golang.org/x/sync v0.12.0 // indirect 61 | golang.org/x/sys v0.31.0 // indirect 62 | golang.org/x/term v0.30.0 // indirect 63 | golang.org/x/text v0.23.0 // indirect 64 | golang.org/x/time v0.10.0 // indirect 65 | golang.org/x/tools v0.30.0 // indirect 66 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 67 | google.golang.org/protobuf v1.36.5 // indirect 68 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 69 | gopkg.in/inf.v0 v0.9.1 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | k8s.io/apiextensions-apiserver v0.32.1 // indirect 72 | k8s.io/klog/v2 v2.130.1 // indirect 73 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 74 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 75 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 76 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 77 | sigs.k8s.io/yaml v1.4.0 // indirect 78 | ) 79 | -------------------------------------------------------------------------------- /hack/add-bundle-annotations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # Ensure that 'yq' is installed in the bin directory by running 'make yq' 6 | YQ="$ROOT/bin/yq" 7 | CSV_FILE="bundle/manifests/hcp-terraform-operator.clusterserviceversion.yaml" 8 | 9 | ##### Add 'com.redhat.openshift.versions' annotation. 10 | echo "Add 'com.redhat.openshift.versions' annotation." 11 | OPENSHIFT_VERSIONS="\"v4.12\"" 12 | { 13 | echo "" 14 | echo " # OpenShift specific annotations" 15 | echo " com.redhat.openshift.versions: $OPENSHIFT_VERSIONS" 16 | } >> bundle/metadata/annotations.yaml 17 | 18 | ##### Add 'containerImage' annotation. 19 | echo "Add 'containerImage' annotation." 20 | IMAGE=$(yq '.spec.install.spec.deployments[] | select(.name == "hcp-terraform-operator-controller-manager") | .spec.template.spec.containers[] | select(.name == "manager") | .image' $CSV_FILE) 21 | echo $IMAGE 22 | yq -i ".metadata.annotations.containerImage = \"$IMAGE\"" $CSV_FILE 23 | -------------------------------------------------------------------------------- /hack/add-bundle-replaces.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # Ensure that 'yq' is installed in the bin directory by running 'make yq' 6 | YQ="$ROOT/bin/yq" 7 | CSV_FILE="bundle/manifests/hcp-terraform-operator.clusterserviceversion.yaml" 8 | 9 | echo "Set 'spec.replaces' field." 10 | PREV_VERSION=`git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1)` 11 | echo "Previous version: $PREV_VERSION" 12 | 13 | yq -i ".spec.replaces = \"hcp-terraform-operator.$PREV_VERSION\"" $CSV_FILE 14 | yq -i "sort_keys(..)" $CSV_FILE 15 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | -------------------------------------------------------------------------------- /internal/controller/consts.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "time" 8 | ) 9 | 10 | // SHARED CONSTANTS 11 | const ( 12 | annotationTrue = "true" 13 | annotationFalse = "false" 14 | maxPageSize = 100 15 | requeueInterval = 15 * time.Second 16 | runMessage = "Triggered by HCP Terraform Operator" 17 | ) 18 | 19 | // AGENT POOL CONTROLLER'S CONSTANTS 20 | const ( 21 | agentPoolFinalizer = "agentpool.app.terraform.io/finalizer" 22 | ) 23 | 24 | // MODULE CONTROLLER'S CONSTANTS 25 | const ( 26 | requeueConfigurationUploadInterval = 10 * time.Second 27 | requeueNewRunInterval = 10 * time.Second 28 | requeueRunStatusInterval = 30 * time.Second 29 | moduleFinalizer = "module.app.terraform.io/finalizer" 30 | 31 | moduleTemplate = ` 32 | {{- $moduleName := .Name -}} 33 | {{- if .Variables }} 34 | {{ range $v := .Variables }} 35 | variable "{{ $v.Name }}" {} 36 | {{- end}} 37 | {{- end }} 38 | 39 | module "{{ $moduleName }}" { 40 | source = "{{ .Module.Source }}" 41 | {{- if .Module.Version }} 42 | version = "{{ .Module.Version }}" 43 | {{- end }} 44 | 45 | {{- if .Variables }} 46 | {{ range $v := .Variables }} 47 | {{ $v.Name }} = var.{{ $v.Name }} 48 | {{- end}} 49 | {{- end }} 50 | } 51 | 52 | {{- if .Outputs }} 53 | {{ range $o := .Outputs }} 54 | output "{{ $o.Name }}" { 55 | value = module.{{ $moduleName }}.{{ $o.Name }} 56 | sensitive = {{ $o.Sensitive }} 57 | } 58 | {{- end}} 59 | {{- end }} 60 | ` 61 | ) 62 | 63 | // PROJECT CONTROLLER'S CONSTANTS 64 | const ( 65 | projectFinalizer = "project.app.terraform.io/finalizer" 66 | ) 67 | 68 | // WORKSPACE CONTROLLER'S CONSTANTS 69 | const ( 70 | workspaceFinalizerAlpha1 = "finalizer.workspace.app.terraform.io" 71 | workspaceFinalizer = "workspace.app.terraform.io/finalizer" 72 | 73 | workspaceAnnotationRunNew = "workspace.app.terraform.io/run-new" 74 | workspaceAnnotationRunType = "workspace.app.terraform.io/run-type" 75 | workspaceAnnotationRunTerraformVersion = "workspace.app.terraform.io/run-terraform-version" 76 | 77 | runTypePlan = "plan" 78 | runTypeApply = "apply" 79 | runTypeRefresh = "refresh" 80 | runTypeDefault = runTypePlan 81 | ) 82 | -------------------------------------------------------------------------------- /internal/controller/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "time" 8 | ) 9 | 10 | var ( 11 | AgentPoolSyncPeriod time.Duration 12 | ModuleSyncPeriod time.Duration 13 | ProjectSyncPeriod time.Duration 14 | WorkspaceSyncPeriod time.Duration 15 | ) 16 | -------------------------------------------------------------------------------- /internal/controller/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | tfc "github.com/hashicorp/go-tfe" 16 | corev1 "k8s.io/api/core/v1" 17 | "k8s.io/apimachinery/pkg/types" 18 | "sigs.k8s.io/controller-runtime/pkg/client" 19 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 20 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 21 | ) 22 | 23 | func doNotRequeue() (reconcile.Result, error) { 24 | return reconcile.Result{}, nil 25 | } 26 | 27 | func requeueAfter(duration time.Duration) (reconcile.Result, error) { 28 | return reconcile.Result{Requeue: true, RequeueAfter: duration}, nil 29 | } 30 | 31 | func requeueOnErr(err error) (reconcile.Result, error) { 32 | return reconcile.Result{}, err 33 | } 34 | 35 | // formatOutput formats TFC/E output to a string or bytes to save it further in 36 | // Kubernetes ConfigMap or Secret, respectively. 37 | // 38 | // Terraform supports the following types: 39 | // - https://developer.hashicorp.com/terraform/language/expressions/types 40 | // When the output value is `null`(special value), TFC/E does not return it. 41 | // Thus, we do not catch it here. 42 | func formatOutput(o *tfc.StateVersionOutput) (string, error) { 43 | switch x := o.Value.(type) { 44 | case bool: 45 | return strconv.FormatBool(x), nil 46 | case float64: 47 | return fmt.Sprint(x), nil 48 | case string: 49 | return x, nil 50 | default: 51 | b, err := json.Marshal(o.Value) 52 | if err != nil { 53 | return "", err 54 | } 55 | return string(b), nil 56 | } 57 | } 58 | 59 | type Object interface { 60 | client.Object 61 | } 62 | 63 | // needToAddFinalizer reports true when a given object doesn't contain a given finalizer and it is not marked for deletion. 64 | // Otherwise, it reports false. 65 | func needToAddFinalizer[T Object](o T, finalizer string) bool { 66 | return o.GetDeletionTimestamp().IsZero() && !controllerutil.ContainsFinalizer(o, finalizer) 67 | } 68 | 69 | // isDeletionCandidate reports true when a given object contains a given finalizer and it is marked for deletion. 70 | // Otherwise, it reports false. 71 | func isDeletionCandidate[T Object](o T, finalizer string) bool { 72 | return !o.GetDeletionTimestamp().IsZero() && controllerutil.ContainsFinalizer(o, finalizer) 73 | } 74 | 75 | // configMapKeyRef fetches a given key name from a given Kubernetes Config Map. 76 | func configMapKeyRef(ctx context.Context, c client.Client, nn types.NamespacedName, key string) (string, error) { 77 | cm := &corev1.ConfigMap{} 78 | if err := c.Get(ctx, nn, cm); err != nil { 79 | return "", err 80 | } 81 | 82 | if k, ok := cm.Data[key]; ok { 83 | return k, nil 84 | } 85 | 86 | return "", fmt.Errorf("unable to find key=%q in configMap=%q namespace=%q", key, nn.Name, nn.Namespace) 87 | } 88 | 89 | // secretKeyRef fetches a given key name from a given Kubernetes Secret. 90 | func secretKeyRef(ctx context.Context, c client.Client, nn types.NamespacedName, key string) (string, error) { 91 | secret := &corev1.Secret{} 92 | if err := c.Get(ctx, nn, secret); err != nil { 93 | return "", err 94 | } 95 | 96 | if k, ok := secret.Data[key]; ok { 97 | return strings.TrimSpace(string(k)), nil 98 | } 99 | 100 | return "", fmt.Errorf("unable to find key=%q in secret=%q namespace=%q", key, nn.Name, nn.Namespace) 101 | } 102 | 103 | func parseTFEVersion(version string) (int, error) { 104 | versionRegexp := regexp.MustCompile(`^v([0-9]{6})-([0-9]{1})$`) 105 | matches := versionRegexp.FindStringSubmatch(version) 106 | if len(matches) == 3 { 107 | return strconv.Atoi(matches[1] + matches[2]) 108 | } 109 | 110 | return 0, fmt.Errorf("malformed TFE version %s", version) 111 | } 112 | -------------------------------------------------------------------------------- /internal/controller/module_controller_workspace.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | 10 | tfc "github.com/hashicorp/go-tfe" 11 | ) 12 | 13 | func (r *ModuleReconciler) getWorkspaceByName(ctx context.Context, m *moduleInstance) (*tfc.Workspace, error) { 14 | return m.tfClient.Client.Workspaces.Read(ctx, m.instance.Spec.Organization, m.instance.Spec.Workspace.Name) 15 | } 16 | 17 | func (r *ModuleReconciler) getWorkspaceByID(ctx context.Context, m *moduleInstance) (*tfc.Workspace, error) { 18 | return m.tfClient.Client.Workspaces.ReadByID(ctx, m.instance.Spec.Workspace.ID) 19 | } 20 | 21 | func (r *ModuleReconciler) getWorkspace(ctx context.Context, m *moduleInstance) (*tfc.Workspace, error) { 22 | specWorkspace := m.instance.Spec.Workspace 23 | 24 | if specWorkspace == nil { 25 | return &tfc.Workspace{}, errors.New("instance.Spec.Workspace is nil") 26 | } 27 | 28 | if specWorkspace.Name != "" { 29 | m.log.Info("Reconcile Module Workspace", "msg", "getting workspace by name") 30 | return r.getWorkspaceByName(ctx, m) 31 | } 32 | 33 | m.log.Info("Reconcile Module Workspace", "msg", "getting workspace by ID") 34 | return r.getWorkspaceByID(ctx, m) 35 | } 36 | -------------------------------------------------------------------------------- /internal/controller/project_controller_deletion_policy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | tfc "github.com/hashicorp/go-tfe" 11 | corev1 "k8s.io/api/core/v1" 12 | 13 | appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2" 14 | ) 15 | 16 | func (r *ProjectReconciler) deleteProject(ctx context.Context, p *projectInstance) error { 17 | p.log.Info("Reconcile Project", "msg", fmt.Sprintf("deletion policy is %s", p.instance.Spec.DeletionPolicy)) 18 | 19 | if p.instance.Status.ID == "" { 20 | p.log.Info("Reconcile Project", "msg", fmt.Sprintf("status.ID is empty, remove finalizer %s", projectFinalizer)) 21 | return r.removeFinalizer(ctx, p) 22 | } 23 | 24 | switch p.instance.Spec.DeletionPolicy { 25 | case appv1alpha2.ProjectDeletionPolicyRetain: 26 | p.log.Info("Reconcile Project", "msg", fmt.Sprintf("remove finalizer %s", projectFinalizer)) 27 | return r.removeFinalizer(ctx, p) 28 | case appv1alpha2.ProjectDeletionPolicySoft: 29 | err := p.tfClient.Client.Projects.Delete(ctx, p.instance.Status.ID) 30 | if err != nil { 31 | if err == tfc.ErrResourceNotFound { 32 | p.log.Info("Reconcile Project", "msg", "Project was not found, remove finalizer") 33 | return r.removeFinalizer(ctx, p) 34 | } 35 | p.log.Error(err, "Reconcile Project", "msg", fmt.Sprintf("failed to delete project ID %s, retry later", p.instance.Status.ID)) 36 | r.Recorder.Eventf(&p.instance, corev1.EventTypeWarning, "Reconcile Project", "Failed to destroy project ID %s, retry later", p.instance.Status.ID) 37 | return err 38 | } 39 | 40 | p.log.Info("Reconcile Project", "msg", fmt.Sprintf("Project ID %s has been deleted, remove finalizer", p.instance.Status.ID)) 41 | return r.removeFinalizer(ctx, p) 42 | 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/controller/workspace_controller_agents.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | tfc "github.com/hashicorp/go-tfe" 11 | ) 12 | 13 | func (r *WorkspaceReconciler) getAgentPoolIDByName(ctx context.Context, w *workspaceInstance) (string, error) { 14 | agentPoolName := w.instance.Spec.AgentPool.Name 15 | 16 | listOpts := &tfc.AgentPoolListOptions{ 17 | Query: agentPoolName, 18 | ListOptions: tfc.ListOptions{ 19 | PageSize: maxPageSize, 20 | }, 21 | } 22 | for { 23 | agentPoolIDs, err := w.tfClient.Client.AgentPools.List(ctx, w.instance.Spec.Organization, listOpts) 24 | if err != nil { 25 | return "", err 26 | } 27 | for _, a := range agentPoolIDs.Items { 28 | if a.Name == agentPoolName { 29 | return a.ID, nil 30 | } 31 | } 32 | if agentPoolIDs.NextPage == 0 { 33 | break 34 | } 35 | listOpts.PageNumber = agentPoolIDs.NextPage 36 | } 37 | 38 | return "", fmt.Errorf("agent pool ID not found for agent pool name %q", agentPoolName) 39 | } 40 | 41 | func (r *WorkspaceReconciler) getAgentPoolID(ctx context.Context, w *workspaceInstance) (string, error) { 42 | specAgentPool := w.instance.Spec.AgentPool 43 | 44 | if specAgentPool == nil { 45 | return "", fmt.Errorf("'spec.agentPool' is not set") 46 | } 47 | 48 | if specAgentPool.Name != "" { 49 | w.log.Info("Reconcile Agent Pool", "msg", "getting agent pool ID by name") 50 | return r.getAgentPoolIDByName(ctx, w) 51 | } 52 | 53 | w.log.Info("Reconcile Agent Pool", "msg", "getting agent pool ID from the spec.AgentPool.ID") 54 | return specAgentPool.ID, nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/controller/workspace_controller_projects.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | tfc "github.com/hashicorp/go-tfe" 11 | ) 12 | 13 | func (w *workspaceInstance) getProjectIDByName(ctx context.Context) (string, error) { 14 | projectName := w.instance.Spec.Project.Name 15 | 16 | listOpts := &tfc.ProjectListOptions{ 17 | Name: projectName, 18 | ListOptions: tfc.ListOptions{ 19 | PageSize: maxPageSize, 20 | }, 21 | } 22 | for { 23 | projectIDs, err := w.tfClient.Client.Projects.List(ctx, w.instance.Spec.Organization, listOpts) 24 | if err != nil { 25 | return "", err 26 | } 27 | for _, p := range projectIDs.Items { 28 | if p.Name == projectName { 29 | return p.ID, nil 30 | } 31 | } 32 | if projectIDs.NextPage == 0 { 33 | break 34 | } 35 | listOpts.PageNumber = projectIDs.NextPage 36 | } 37 | 38 | return "", fmt.Errorf("project ID not found for project name %q", projectName) 39 | } 40 | 41 | func (w *workspaceInstance) getProjectID(ctx context.Context) (string, error) { 42 | specProject := w.instance.Spec.Project 43 | 44 | if specProject == nil { 45 | return "", fmt.Errorf("'spec.Project' is not set") 46 | } 47 | 48 | if specProject.Name != "" { 49 | w.log.Info("Reconcile Project", "msg", "getting project ID by name") 50 | return w.getProjectIDByName(ctx) 51 | } 52 | 53 | w.log.Info("Reconcile Project", "msg", "getting project ID from the spec.Project.ID") 54 | return specProject.ID, nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/controller/workspace_controller_remote_state_sharing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | 10 | tfc "github.com/hashicorp/go-tfe" 11 | appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2" 12 | corev1 "k8s.io/api/core/v1" 13 | ) 14 | 15 | func (r *WorkspaceReconciler) getWorkspaces(ctx context.Context, w *workspaceInstance) (map[string]string, error) { 16 | o := make(map[string]string) 17 | 18 | listOpts := &tfc.WorkspaceListOptions{ 19 | ListOptions: tfc.ListOptions{ 20 | PageSize: maxPageSize, 21 | }, 22 | } 23 | for { 24 | ws, err := w.tfClient.Client.Workspaces.List(ctx, w.instance.Spec.Organization, listOpts) 25 | if err != nil { 26 | return map[string]string{}, err 27 | } 28 | for _, w := range ws.Items { 29 | o[w.ID] = w.ID 30 | o[w.Name] = w.ID 31 | } 32 | if ws.NextPage == 0 { 33 | break 34 | } 35 | listOpts.PageNumber = ws.NextPage 36 | } 37 | 38 | return o, nil 39 | } 40 | 41 | // nameOrID returns Name or ID from the passed structure where only one of them is set 42 | func nameOrID(instanceWorkspace *appv1alpha2.ConsumerWorkspace) string { 43 | if instanceWorkspace.Name != "" { 44 | return instanceWorkspace.Name 45 | } 46 | 47 | return instanceWorkspace.ID 48 | } 49 | 50 | func getWorkspaceID(workspaces map[string]string, instanceWorkspace *appv1alpha2.ConsumerWorkspace) (string, error) { 51 | k := nameOrID(instanceWorkspace) 52 | if v, ok := workspaces[k]; ok { 53 | return v, nil 54 | } 55 | return "", errors.New("workspace ID not found") 56 | } 57 | 58 | func (r *WorkspaceReconciler) getInstanceRemoteStateSharing(ctx context.Context, w *workspaceInstance) ([]*tfc.Workspace, error) { 59 | iw := []*tfc.Workspace{} 60 | 61 | if len(w.instance.Spec.RemoteStateSharing.Workspaces) == 0 { 62 | return iw, nil 63 | } 64 | 65 | workspaces, err := r.getWorkspaces(ctx, w) 66 | if err != nil { 67 | return iw, err 68 | } 69 | 70 | for _, workspace := range w.instance.Spec.RemoteStateSharing.Workspaces { 71 | wID, err := getWorkspaceID(workspaces, workspace) 72 | if err != nil { 73 | w.log.Error(err, "Reconcile Remote State Sharing", "msg", "failed to get workspace ID") 74 | r.Recorder.Event(&w.instance, corev1.EventTypeWarning, "ReconcileRemoteStateSharing", "Failed to get workspace ID") 75 | return iw, err 76 | } 77 | iw = append(iw, &tfc.Workspace{ID: wID}) 78 | } 79 | 80 | return iw, nil 81 | } 82 | 83 | func (r *WorkspaceReconciler) reconcileRemoteStateSharing(ctx context.Context, w *workspaceInstance) error { 84 | w.log.Info("Reconcile Remote State Sharing", "msg", "new reconciliation event") 85 | 86 | if w.instance.Spec.RemoteStateSharing == nil { 87 | return nil 88 | } 89 | 90 | instanceRemoteStateSharing, err := r.getInstanceRemoteStateSharing(ctx, w) 91 | if err != nil { 92 | w.log.Error(err, "Reconcile Remote State Sharing", "msg", "failed to get instance remote state sharing workspace sources") 93 | return err 94 | } 95 | 96 | if len(instanceRemoteStateSharing) > 0 { 97 | err = w.tfClient.Client.Workspaces.UpdateRemoteStateConsumers(ctx, w.instance.Status.WorkspaceID, tfc.WorkspaceUpdateRemoteStateConsumersOptions{ 98 | Workspaces: instanceRemoteStateSharing, 99 | }) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/controller/workspace_controller_sshkey.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | 11 | tfc "github.com/hashicorp/go-tfe" 12 | ) 13 | 14 | func (w *workspaceInstance) getSSHKeyID(ctx context.Context) (string, error) { 15 | if w.instance.Spec.SSHKey == nil { 16 | return "", errors.New("instance spec.SSHKey is nil") 17 | } 18 | 19 | if w.instance.Spec.SSHKey.Name != "" { 20 | w.log.Info("Reconcile SSH Key", "msg", "getting SSH key ID by name") 21 | listOpts := &tfc.SSHKeyListOptions{ 22 | ListOptions: tfc.ListOptions{ 23 | PageNumber: 1, 24 | PageSize: maxPageSize, 25 | }, 26 | } 27 | for { 28 | sshKeyList, err := w.tfClient.Client.SSHKeys.List(ctx, w.instance.Spec.Organization, listOpts) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | for _, s := range sshKeyList.Items { 34 | if s.Name == w.instance.Spec.SSHKey.Name { 35 | return s.ID, nil 36 | } 37 | } 38 | 39 | if sshKeyList.NextPage == 0 { 40 | break 41 | } 42 | listOpts.PageNumber = sshKeyList.NextPage 43 | } 44 | return "", fmt.Errorf("ssh key ID was not found for SSH key name %q", w.instance.Spec.SSHKey.Name) 45 | } 46 | 47 | w.log.Info("Reconcile SSH Key", "msg", "getting SSH key ID from the spec.sshKey.ID") 48 | return w.instance.Spec.SSHKey.ID, nil 49 | } 50 | 51 | func (w *workspaceInstance) reconcileSSHKey(ctx context.Context, workspace *tfc.Workspace) error { 52 | w.log.Info("Reconcile SSH Key", "msg", "new reconciliation event") 53 | 54 | if w.instance.Spec.SSHKey == nil && workspace.SSHKey != nil { 55 | w.log.Info("Reconcile SSH Key", "msg", "removing the SSH key") 56 | if _, err := w.tfClient.Client.Workspaces.UnassignSSHKey(ctx, workspace.ID); err != nil { 57 | return err 58 | } 59 | w.instance.Status.SSHKeyID = "" 60 | } 61 | 62 | if w.instance.Spec.SSHKey != nil { 63 | if workspace.SSHKey == nil || workspace.SSHKey.ID != w.instance.Status.SSHKeyID { 64 | sshKeyID, err := w.getSSHKeyID(ctx) 65 | if err != nil { 66 | return err 67 | } 68 | w.log.Info("Reconcile SSH Key", "msg", "assigning the SSH key") 69 | opt := tfc.WorkspaceAssignSSHKeyOptions{ 70 | SSHKeyID: &sshKeyID, 71 | } 72 | workspace, err = w.tfClient.Client.Workspaces.AssignSSHKey(ctx, workspace.ID, opt) 73 | if err != nil { 74 | return err 75 | } 76 | w.instance.Status.SSHKeyID = workspace.SSHKey.ID 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/controller/workspace_controller_tags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | 9 | tfc "github.com/hashicorp/go-tfe" 10 | appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2" 11 | ) 12 | 13 | // HELPERS 14 | 15 | // getTags return a map that maps consist of all tags defined in a object specification(spec.tags) 16 | // and values 'true' to simulate the Set structure 17 | func getTags(instance *appv1alpha2.Workspace) map[string]bool { 18 | tags := make(map[string]bool) 19 | 20 | if len(instance.Spec.Tags) == 0 { 21 | return tags 22 | } 23 | 24 | for _, t := range instance.Spec.Tags { 25 | tags[string(t)] = true 26 | } 27 | 28 | return tags 29 | } 30 | 31 | // getWorkspaceTags return a map that maps consist of all tags assigned to workspace 32 | // and values 'true' to simulate the Set structure 33 | func getWorkspaceTags(workspace *tfc.Workspace) map[string]bool { 34 | tags := make(map[string]bool) 35 | 36 | if len(workspace.TagNames) == 0 { 37 | return tags 38 | } 39 | 40 | for _, t := range workspace.TagNames { 41 | tags[t] = true 42 | } 43 | 44 | return tags 45 | } 46 | 47 | // getTagsToAdd returns a list of tags that need to be added to the workspace. 48 | func getTagsToAdd(instanceTags, workspaceTags map[string]bool) []*tfc.Tag { 49 | return tagDifference(instanceTags, workspaceTags) 50 | } 51 | 52 | // getTagsToRemove returns a list of tags that need to be removed from the workspace. 53 | func getTagsToRemove(instanceTags, workspaceTags map[string]bool) []*tfc.Tag { 54 | return tagDifference(workspaceTags, instanceTags) 55 | } 56 | 57 | // tagDifference returns the list of tags(type tfc.Tag) that consists of the elements of leftTags 58 | // which are not elements of rightTags 59 | func tagDifference(leftTags, rightTags map[string]bool) []*tfc.Tag { 60 | var d []*tfc.Tag 61 | 62 | for t := range leftTags { 63 | if !rightTags[t] { 64 | d = append(d, &tfc.Tag{Name: t}) 65 | } 66 | } 67 | 68 | return d 69 | } 70 | 71 | // addWorkspaceTags adds tags to workspace 72 | func (r *WorkspaceReconciler) addWorkspaceTags(ctx context.Context, w *workspaceInstance, tags []*tfc.Tag) error { 73 | if len(tags) == 0 { 74 | return nil 75 | } 76 | 77 | return w.tfClient.Client.Workspaces.AddTags(ctx, w.instance.Status.WorkspaceID, tfc.WorkspaceAddTagsOptions{Tags: tags}) 78 | } 79 | 80 | // removeWorkspaceTags removes tags from workspace 81 | func (r *WorkspaceReconciler) removeWorkspaceTags(ctx context.Context, w *workspaceInstance, tags []*tfc.Tag) error { 82 | if len(tags) == 0 { 83 | return nil 84 | } 85 | 86 | return w.tfClient.Client.Workspaces.RemoveTags(ctx, w.instance.Status.WorkspaceID, tfc.WorkspaceRemoveTagsOptions{Tags: tags}) 87 | } 88 | 89 | func (r *WorkspaceReconciler) reconcileTags(ctx context.Context, w *workspaceInstance, workspace *tfc.Workspace) error { 90 | w.log.Info("Reconcile Tags", "msg", "new reconciliation event") 91 | 92 | instanceTags := getTags(&w.instance) 93 | workspaceTags := getWorkspaceTags(workspace) 94 | 95 | removeTags := getTagsToRemove(instanceTags, workspaceTags) 96 | if len(removeTags) > 0 { 97 | w.log.Info("Reconcile Tags", "msg", "removing tags from the workspace") 98 | err := r.removeWorkspaceTags(ctx, w, removeTags) 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | 104 | addTags := getTagsToAdd(instanceTags, workspaceTags) 105 | if len(addTags) > 0 { 106 | w.log.Info("Reconcile Tags", "msg", "adding tags from the workspace") 107 | err := r.addWorkspaceTags(ctx, w, addTags) 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /internal/controller/workspace_controller_tags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | tfc "github.com/hashicorp/go-tfe" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | // TODO these are unit tests, lets just do these as vanilla Go tests 13 | var _ = Describe("Helpers", func() { 14 | Context("Tags", func() { 15 | It("returns difference between tags sets", func() { 16 | leftTags := map[string]bool{ 17 | "A": true, 18 | "B": true, 19 | } 20 | rightTags := map[string]bool{ 21 | "B": true, 22 | } 23 | expect := []*tfc.Tag{ 24 | {Name: "A"}, 25 | } 26 | d := tagDifference(leftTags, rightTags) 27 | 28 | Expect(d).Should(ConsistOf(expect)) 29 | }) 30 | It("returns leftTags as difference between unintersectioned tags sets", func() { 31 | leftTags := map[string]bool{ 32 | "A": true, 33 | "B": true, 34 | } 35 | rightTags := map[string]bool{ 36 | "C": true, 37 | "D": true, 38 | } 39 | expect := []*tfc.Tag{ 40 | {Name: "A"}, 41 | {Name: "B"}, 42 | } 43 | d := tagDifference(leftTags, rightTags) 44 | Expect(d).Should(ConsistOf(expect)) 45 | }) 46 | It("returns leftTags as difference between tags sets when rightTags set is empty", func() { 47 | leftTags := map[string]bool{ 48 | "A": true, 49 | "B": true, 50 | } 51 | rightTags := make(map[string]bool) 52 | expect := []*tfc.Tag{ 53 | {Name: "A"}, 54 | {Name: "B"}, 55 | } 56 | d := tagDifference(leftTags, rightTags) 57 | Expect(d).Should(ConsistOf(expect)) 58 | }) 59 | It("returns no difference between tags sets when leftTags set is empty", func() { 60 | leftTags := map[string]bool{} 61 | rightTags := map[string]bool{ 62 | "A": true, 63 | "B": true, 64 | } 65 | var expect []*tfc.Tag 66 | d := tagDifference(leftTags, rightTags) 67 | Expect(d).Should(ConsistOf(expect)) 68 | }) 69 | It("returns no difference between equal tags sets", func() { 70 | leftTags := map[string]bool{ 71 | "A": true, 72 | "B": true, 73 | } 74 | rightTags := map[string]bool{ 75 | "B": true, 76 | "A": true, 77 | } 78 | var expect []*tfc.Tag 79 | d := tagDifference(leftTags, rightTags) 80 | Expect(d).Should(ConsistOf(expect)) 81 | }) 82 | It("returns no difference between empty tags sets", func() { 83 | leftTags := map[string]bool{} 84 | rightTags := map[string]bool{} 85 | var expect []*tfc.Tag 86 | d := tagDifference(leftTags, rightTags) 87 | Expect(d).Should(ConsistOf(expect)) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /internal/controller/workspace_controller_vcs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | tfc "github.com/hashicorp/go-tfe" 8 | appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2" 9 | ) 10 | 11 | // getTriggerPatterns return a map that maps consist of all trigger patterns defined in a object specification 12 | // and values 'true' to simulate the Set structure. 13 | func getTriggerPatterns(instance *appv1alpha2.Workspace) map[string]struct{} { 14 | patterns := make(map[string]struct{}) 15 | 16 | if instance.Spec.VersionControl == nil || len(instance.Spec.VersionControl.TriggerPatterns) == 0 { 17 | return patterns 18 | } 19 | 20 | for _, t := range instance.Spec.VersionControl.TriggerPatterns { 21 | patterns[string(t)] = struct{}{} 22 | } 23 | 24 | return patterns 25 | } 26 | 27 | // getWorkspaceTriggerPatterns return a map that maps consist of all trigger patterns assigned to workspace 28 | // and values 'true' to simulate the Set structure. 29 | func getWorkspaceTriggerPatterns(workspace *tfc.Workspace) map[string]struct{} { 30 | patterns := make(map[string]struct{}) 31 | 32 | if len(workspace.TriggerPatterns) == 0 { 33 | return patterns 34 | } 35 | 36 | for _, t := range workspace.TriggerPatterns { 37 | patterns[t] = struct{}{} 38 | } 39 | 40 | return patterns 41 | } 42 | 43 | // getTriggerPrefixes return a map that maps consist of all trigger prefixes defined in a object specification 44 | // and values 'true' to simulate the Set structure. 45 | func getTriggerPrefixes(instance *appv1alpha2.Workspace) map[string]struct{} { 46 | prefixes := make(map[string]struct{}) 47 | 48 | if instance.Spec.VersionControl == nil || len(instance.Spec.VersionControl.TriggerPrefixes) == 0 { 49 | return prefixes 50 | } 51 | 52 | for _, t := range instance.Spec.VersionControl.TriggerPrefixes { 53 | prefixes[string(t)] = struct{}{} 54 | } 55 | 56 | return prefixes 57 | } 58 | 59 | // getWorkspaceTriggerPrefixes return a map that maps consist of all trigger prefixes assigned to workspace 60 | // and values 'true' to simulate the Set structure. 61 | func getWorkspaceTriggerPrefixes(workspace *tfc.Workspace) map[string]struct{} { 62 | prefixes := make(map[string]struct{}) 63 | 64 | if len(workspace.TriggerPrefixes) == 0 { 65 | return prefixes 66 | } 67 | 68 | for _, t := range workspace.TriggerPrefixes { 69 | prefixes[t] = struct{}{} 70 | } 71 | 72 | return prefixes 73 | } 74 | 75 | // vcsTriggersDifference returns the list of file trigger prefixes that consists of the elements of leftTriggers 76 | // which are not elements of rightTriggers. 77 | func vcsTriggersDifference(leftTriggers, rightTriggers map[string]struct{}) []string { 78 | var d []string 79 | 80 | for t := range leftTriggers { 81 | if _, ok := rightTriggers[t]; !ok { 82 | d = append(d, t) 83 | } 84 | } 85 | 86 | return d 87 | } 88 | -------------------------------------------------------------------------------- /internal/pointer/pointer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package pointer 5 | 6 | func PointerOf[A any](a A) *A { 7 | return &a 8 | } 9 | -------------------------------------------------------------------------------- /internal/pointer/pointer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package pointer 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestPointerOf(t *testing.T) { 11 | s := "this" 12 | sp := PointerOf(s) 13 | if s != *sp { 14 | t.Error("Failed to get string pointer") 15 | } 16 | 17 | i := int(1984) 18 | ip := PointerOf(i) 19 | if i != *ip { 20 | t.Error("Failed to get int pointer") 21 | } 22 | 23 | i64 := int64(1984) 24 | i64p := PointerOf(i64) 25 | if i64 != *i64p { 26 | t.Error("Failed to get int64 pointer") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/slice/slice.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package slice 5 | 6 | func RemoveFromSlice[A any](slice []A, i int) []A { 7 | return append(slice[:i], slice[i+1:]...) 8 | } 9 | -------------------------------------------------------------------------------- /internal/slice/slice_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package slice 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestRemoveFromSlice(t *testing.T) { 12 | input := [][]any{ 13 | {1, 2, 3}, 14 | {"a", "b", "c"}, 15 | } 16 | want := [][]any{ 17 | {1, 3}, 18 | {"a", "c"}, 19 | } 20 | for i, s := range input { 21 | r := RemoveFromSlice(s, 1) 22 | if !reflect.DeepEqual(want[i], r) { 23 | t.Errorf("Failed to remove an element from slice %d", i) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/update-helm-chart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | CHART_DIR="charts/hcp-terraform-operator" 7 | CHART_FILE="Chart.yaml" 8 | 9 | # Update the 'Chart.yaml' file with a new version of the Operator image tag. 10 | function update_chart_file { 11 | C_VERSION=`yq '.appVersion' $CHART_DIR/$CHART_FILE` 12 | C_CHART_VERSION=`yq '.version' $CHART_DIR/$CHART_FILE` 13 | 14 | if [[ $C_VERSION == $HCP_TF_OPERATOR_RELEASE_VERSION && $C_CHART_VERSION == $HCP_TF_OPERATOR_RELEASE_VERSION ]]; then 15 | echo "No changes in the $CHART_FILE file." 16 | return 0 17 | fi 18 | 19 | echo "Updating the $CHART_FILE file." 20 | 21 | yq \ 22 | --inplace \ 23 | '.appVersion = strenv(HCP_TF_OPERATOR_RELEASE_VERSION) | .version = strenv(HCP_TF_OPERATOR_RELEASE_VERSION)' $CHART_DIR/$CHART_FILE 24 | } 25 | 26 | function main { 27 | if [[ -z "${HCP_TF_OPERATOR_RELEASE_VERSION}" ]]; then 28 | echo "The environment variable HCP_TF_OPERATOR_RELEASE_VERSION is not set." 29 | exit 1 30 | fi 31 | 32 | GIT_BRANCH=`git rev-parse --abbrev-ref HEAD | sed -e 's/^release\/v//'` 33 | 34 | if [[ "$HCP_TF_OPERATOR_RELEASE_VERSION" != "$GIT_BRANCH" ]]; then 35 | echo "The version in the git branch name '${GIT_BRANCH}' does not match with the release version '${HCP_TF_OPERATOR_RELEASE_VERSION}'." 36 | echo "Exiting!" 37 | exit 1 38 | fi 39 | 40 | echo "Version: ${HCP_TF_OPERATOR_RELEASE_VERSION}" 41 | 42 | update_chart_file 43 | } 44 | 45 | main 46 | -------------------------------------------------------------------------------- /version/VERSION: -------------------------------------------------------------------------------- 1 | 2.9.2 2 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import "fmt" 7 | 8 | var ( 9 | // The version should remain as 'X.0.0-dev' throughout the entire development cycle of a specific major version X. 10 | // The minor and patch components should remain unchanged. 11 | Version = "2.0.0-dev" 12 | UserAgent = fmt.Sprintf("HCPTerraformOperator/v%s", Version) 13 | // The user agent 'TerraformCloudOperator' was only used for version 2.3.0 and will remain here for visibility. 14 | // It is not commented out to ensure that future generations will not miss it. 15 | _ = fmt.Sprintf("TerraformCloudOperator/v%s", Version) 16 | ) 17 | --------------------------------------------------------------------------------