diff --git a/.gitignore b/.gitignore index 7a3e2fd..7f122c2 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ override.tf.json # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan # example: *tfplan* + +# Terraform Provider lock file +.terraform.lock.hcl diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..34726a8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: +- repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.68.1 # Get the latest from: https://github.com/antonbabenko/pre-commit-terraform/releases + hooks: + - id: terraform_fmt + - id: terraform_docs + args: + - '--args=--lockfile=false' + - id: terraform_validate + - id: terraform_tflint + args: + - '--args=--only=terraform_deprecated_interpolation' + - '--args=--only=terraform_deprecated_index' + - '--args=--only=terraform_unused_declarations' + - '--args=--only=terraform_comment_syntax' + - '--args=--only=terraform_documented_outputs' + - '--args=--only=terraform_documented_variables' + - '--args=--only=terraform_typed_variables' + - '--args=--only=terraform_module_pinned_source' + - '--args=--only=terraform_naming_convention' + - '--args=--only=terraform_required_version' + - '--args=--only=terraform_required_providers' + - '--args=--only=terraform_standard_module_structure' + - '--args=--only=terraform_workspace_remote' +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer diff --git a/README.md b/README.md index e42a6de..7ecc3d0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,69 @@ # terraform-tfe-workspace Terraform module to provision and manage Terraform Cloud workspaces + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.14.0 | +| [tfe](#requirement\_tfe) | ~> 0.31.0 | + +## Providers + +| Name | Version | +|------|---------| +| [tfe](#provider\_tfe) | ~> 0.31.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [tfe_run_trigger.this](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/run_trigger) | resource | +| [tfe_variable.this](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/variable) | resource | +| [tfe_workspace.this](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/workspace) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [allow\_destroy\_plan](#input\_allow\_destroy\_plan) | (Optional) Whether destroy plans can be queued on the workspace | `bool` | `false` | no | +| [auto\_apply](#input\_auto\_apply) | (Optional) Whether to automatically apply changes when a Terraform plan is successful | `bool` | `false` | no | +| [description](#input\_description) | (Optional) A description for the workspace | `string` | `""` | no | +| [environment\_sensitive\_variables](#input\_environment\_sensitive\_variables) | (Optional) Map of sensitive variables of 'environment' category used in the workspace

Item syntax:
{
variable1\_name = value1
variable2\_name = value2
...
} | `map(any)` | `{}` | no | +| [environment\_variables](#input\_environment\_variables) | (Optional) Map of variables of 'environment' category used in the workspace

Item syntax:
{
variable1\_name = value1
variable2\_name = value2
...
} | `map(any)` | `{}` | no | +| [execution\_mode](#input\_execution\_mode) | (Optional) Which execution mode to use | `string` | `"remote"` | no | +| [file\_triggers\_enabled](#input\_file\_triggers\_enabled) | (Optional) Whether to filter runs based on the changed files in a VCS push | `bool` | `true` | no | +| [global\_remote\_state](#input\_global\_remote\_state) | (Optional) Whether the workspace allows all workspaces in the organization to access its state data during runs | `bool` | `false` | no | +| [name](#input\_name) | (Required) Name of the workspace | `string` | n/a | yes | +| [oauth\_token\_id](#input\_oauth\_token\_id) | (Optional) The token ID of the VCS connection to use | `string` | `""` | no | +| [organization](#input\_organization) | (Required) Name of the organization | `string` | n/a | yes | +| [queue\_all\_runs](#input\_queue\_all\_runs) | (Optional) Whether the workspace should start automatically performing runs immediately after its creation | `bool` | `true` | no | +| [remote\_state\_consumer\_ids](#input\_remote\_state\_consumer\_ids) | (Optional) The set of workspace IDs set as explicit remote state consumers for the given workspace | `list(string)` | `[]` | no | +| [run\_triggers](#input\_run\_triggers) | List of source workspaces IDs that trigger runs in this workspace | `list(string)` | `[]` | no | +| [speculative\_enabled](#input\_speculative\_enabled) | (Optional) Whether this workspace allows speculative plans | `bool` | `true` | no | +| [ssh\_key\_id](#input\_ssh\_key\_id) | (Optional) The ID of an SSH key to assign to the workspace | `string` | `null` | no | +| [structured\_run\_output\_enabled](#input\_structured\_run\_output\_enabled) | (Optional) Whether this workspace should show output from Terraform runs using the enhanced UI when available | `bool` | `true` | no | +| [tag\_names](#input\_tag\_names) | (Optional) A list of tag names for this workspace | `list(string)` | `[]` | no | +| [terraform\_hcl\_sensitive\_variables](#input\_terraform\_hcl\_sensitive\_variables) | (Optional) Map of sensitive variables in HCL format of 'Terraform' category used in the workspace

Item syntax:
{
variable1\_name = value1
variable2\_name = value2
...
}

NOTE: you can specifies values in HCL format directly, like this:

{
variable\_list = ["item1","item2"]

variable\_map = {
key1 = value1
key2 = value2
}
} | `any` | `{}` | no | +| [terraform\_hcl\_variables](#input\_terraform\_hcl\_variables) | (Optional) Map of variables in HCL format of 'Terraform' category used in the workspace

Item syntax:
{
variable1\_name = value1
variable2\_name = value2
...
}

NOTE: you can specifies values in HCL format directly, like this:

{
variable\_list = ["item1","item2"]

variable\_map = {
key1 = value1
key2 = value2
}
}
} | `any` | `{}` | no | +| [terraform\_sensitive\_variables](#input\_terraform\_sensitive\_variables) | (Optional) Map of sensitive variables of 'Terraform' category used in the workspace

Item syntax:
{
variable1\_name = value1
variable2\_name = value2
...
} | `map(any)` | `{}` | no | +| [terraform\_variables](#input\_terraform\_variables) | (Optional) Map of variables of 'Terraform' category used in the workspace

Item syntax:
{
variable1\_name = value1
variable2\_name = value2
...
} | `map(any)` | `{}` | no | +| [terraform\_version](#input\_terraform\_version) | (Required) The version of Terraform to use for this workspace | `string` | n/a | yes | +| [trigger\_prefixes](#input\_trigger\_prefixes) | (Optional) List of repository-root-relative paths which describe all locations to be tracked for changes | `list(string)` | `[]` | no | +| [variables\_descriptions](#input\_variables\_descriptions) | (Optional) Map of descriptions applied to workspace variables

Item syntax:
{
variable1\_name = "description"
variable2\_name = "description"
...
} | `map(string)` | `{}` | no | +| [vcs\_repository\_branch](#input\_vcs\_repository\_branch) | (Optional) The repository branch that Terraform will execute from | `string` | `""` | no | +| [vcs\_repository\_identifier](#input\_vcs\_repository\_identifier) | (Optional) A reference to your VCS repository in the format / where and refer to the organization and repository in your VCS provider. The format for Azure DevOps is //\_git/ | `string` | `""` | no | +| [vcs\_repository\_ingress\_submodules](#input\_vcs\_repository\_ingress\_submodules) | (Optional) Whether submodules should be fetched when cloning the VCS repository | `bool` | `false` | no | +| [working\_directory](#input\_working\_directory) | (Optional) A relative path that Terraform will execute within | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [id](#output\_id) | The workspace ID | + diff --git a/examples/simple-workspace/README.md b/examples/simple-workspace/README.md new file mode 100644 index 0000000..6a010ae --- /dev/null +++ b/examples/simple-workspace/README.md @@ -0,0 +1,57 @@ +# terraform-tfe-workspace + +This example will manage a simple Terraform Cloud/Enterprise workspace without +connecting it to the GitHub repository. + +## Usage + +To run this example, you need to execute the following commands: + +```shell +$ terraform init +$ terraform plan +$ terraform apply +``` + +:memo: **Note:** You will need a Terraform Cloud/Enterprise API token for authentication. +You'll be prompted to insert it to provide a value for "tfc_token" variable. +See [here](https://www.terraform.io/cloud-docs/users-teams-organizations/api-tokens) +for further information. + +:warning: **Warning:** This example may create resources that cost money. Execute the command +`terraform destroy` when the resources are no longer needed. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.1.0 | +| [tfe](#requirement\_tfe) | ~>0.31.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [simple\_workspace](#module\_simple\_workspace) | ../../ | n/a | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [tfc\_token](#input\_tfc\_token) | Token for Terraform Cloud Authentication | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [workspace\_id](#output\_workspace\_id) | The ID of Terraform Cloud/Enterprise workspace | + diff --git a/examples/simple-workspace/main.tf b/examples/simple-workspace/main.tf new file mode 100644 index 0000000..66c4521 --- /dev/null +++ b/examples/simple-workspace/main.tf @@ -0,0 +1,35 @@ +terraform { + required_version = "~> 1.1.0" + + required_providers { + tfe = { + source = "hashicorp/tfe" + version = "~>0.31.0" + } + } +} + +provider "tfe" { + token = var.tfc_token +} + +module "simple_workspace" { + source = "../../" + + name = "simple-workspace" + organization = "myorg" + description = "A simple Terraform Cloud/Enterprise workspace" + terraform_version = "1.1.9" + + terraform_variables = { + string_variable = "stringvalue" + number_variable = 1 + bool_variable = true + } + + variables_descriptions = { + string_variable = "A variable containing a value in string format" + number_variable = "A variable containing a value in number format" + bool_variable = "A variable containing a value in boolean format" + } +} diff --git a/examples/simple-workspace/outputs.tf b/examples/simple-workspace/outputs.tf new file mode 100644 index 0000000..a9b3f39 --- /dev/null +++ b/examples/simple-workspace/outputs.tf @@ -0,0 +1,4 @@ +output "workspace_id" { + description = "The ID of Terraform Cloud/Enterprise workspace" + value = module.simple_workspace.id +} diff --git a/examples/simple-workspace/variables.tf b/examples/simple-workspace/variables.tf new file mode 100644 index 0000000..5d77a40 --- /dev/null +++ b/examples/simple-workspace/variables.tf @@ -0,0 +1,5 @@ +variable "tfc_token" { + description = "Token for Terraform Cloud Authentication" + type = string + sensitive = true +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..ca7aa51 --- /dev/null +++ b/main.tf @@ -0,0 +1,115 @@ +locals { + terraform_variables = { for k, v in var.terraform_variables : k => + { + value = v + category = "terraform" + description = lookup(var.variables_descriptions, k, null) + } + } + + terraform_hcl_variables = { for k, v in var.terraform_hcl_variables : k => + { + #NOTE: using @osterman trick https://github.com/hashicorp/terraform-provider-tfe/issues/188#issuecomment-700212045 + value = replace(jsonencode(v), "/(\".*?\"):/", "$1 = ") + category = "terraform" + hcl = true + description = lookup(var.variables_descriptions, k, null) + } + } + + terraform_sensitive_variables = { for k, v in var.terraform_sensitive_variables : k => + { + value = v + category = "terraform" + description = lookup(var.variables_descriptions, k, null) + sensitive = true + } + } + + terraform_hcl_sensitive_variables = { for k, v in var.terraform_hcl_sensitive_variables : k => + { + #NOTE: using @osterman trick https://github.com/hashicorp/terraform-provider-tfe/issues/188#issuecomment-700212045 + value = replace(jsonencode(v), "/(\".*?\"):/", "$1 = ") + category = "terraform" + description = lookup(var.variables_descriptions, k, null) + hcl = true + sensitive = true + } + } + + environment_variables = { for k, v in var.environment_variables : k => + { + value = v + category = "env" + description = lookup(var.variables_descriptions, k, null) + } + } + + environment_sensitive_variables = { for k, v in var.environment_sensitive_variables : k => + { + value = v + category = "env" + description = lookup(var.variables_descriptions, k, null) + sensitive = true + } + } + + all_variables = merge( + local.terraform_variables, + local.terraform_hcl_variables, + local.terraform_sensitive_variables, + local.terraform_hcl_sensitive_variables, + local.environment_variables, + local.environment_sensitive_variables + ) +} + +resource "tfe_workspace" "this" { + name = var.name + organization = var.organization + description = var.description + allow_destroy_plan = var.allow_destroy_plan + auto_apply = var.auto_apply + execution_mode = var.execution_mode + file_triggers_enabled = var.file_triggers_enabled + global_remote_state = var.global_remote_state + remote_state_consumer_ids = var.remote_state_consumer_ids + queue_all_runs = var.queue_all_runs + speculative_enabled = var.speculative_enabled + structured_run_output_enabled = var.structured_run_output_enabled + ssh_key_id = var.ssh_key_id + terraform_version = var.terraform_version + trigger_prefixes = var.trigger_prefixes + tag_names = var.tag_names + working_directory = var.working_directory + + dynamic "vcs_repo" { + for_each = length(var.vcs_repository_identifier) > 0 && length(var.oauth_token_id) > 0 ? [1] : [] + + content { + identifier = var.vcs_repository_identifier + branch = var.vcs_repository_branch + ingress_submodules = var.vcs_repository_ingress_submodules + oauth_token_id = var.oauth_token_id + } + } +} + +resource "tfe_variable" "this" { + for_each = local.all_variables + + key = each.key + value = each.value.value + hcl = try(each.value.hcl, null) + category = each.value.category + description = try(each.value.description, null) + sensitive = try(each.value.sensitive, false) + workspace_id = tfe_workspace.this.id +} + +resource "tfe_run_trigger" "this" { + count = length(var.run_triggers) + + workspace_id = tfe_workspace.this.id + sourceable_id = var.run_triggers[count.index] +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..fcd6e1f --- /dev/null +++ b/outputs.tf @@ -0,0 +1,4 @@ +output "id" { + description = "The workspace ID" + value = tfe_workspace.this.id +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..2f87375 --- /dev/null +++ b/variables.tf @@ -0,0 +1,268 @@ +variable "name" { + description = "(Required) Name of the workspace" + type = string +} + +variable "organization" { + description = "(Required) Name of the organization" + type = string +} + +variable "description" { + description = "(Optional) A description for the workspace" + type = string + default = "" +} + +variable "allow_destroy_plan" { + description = "(Optional) Whether destroy plans can be queued on the workspace" + type = bool + default = false +} + +variable "auto_apply" { + description = "(Optional) Whether to automatically apply changes when a Terraform plan is successful" + type = bool + default = false +} + +variable "execution_mode" { + description = "(Optional) Which execution mode to use" + type = string + default = "remote" + + validation { + condition = can(regex("agent|local|remote", var.execution_mode)) + error_message = "ERROR: Allowed values are \"remote\", \"local\" or \"agent\"." + } +} + +variable "file_triggers_enabled" { + description = "(Optional) Whether to filter runs based on the changed files in a VCS push" + type = bool + default = true +} + +variable "global_remote_state" { + description = "(Optional) Whether the workspace allows all workspaces in the organization to access its state data during runs" + type = bool + default = false +} + +variable "remote_state_consumer_ids" { + description = "(Optional) The set of workspace IDs set as explicit remote state consumers for the given workspace" + type = list(string) + default = [] +} + +variable "queue_all_runs" { + description = "(Optional) Whether the workspace should start automatically performing runs immediately after its creation" + type = bool + default = true +} + +variable "speculative_enabled" { + description = "(Optional) Whether this workspace allows speculative plans" + type = bool + default = true +} + +variable "structured_run_output_enabled" { + description = "(Optional) Whether this workspace should show output from Terraform runs using the enhanced UI when available" + type = bool + default = true +} + +variable "ssh_key_id" { + description = "(Optional) The ID of an SSH key to assign to the workspace" + type = string + default = null +} + +variable "terraform_version" { + description = "(Required) The version of Terraform to use for this workspace" + type = string +} + +variable "trigger_prefixes" { + description = "(Optional) List of repository-root-relative paths which describe all locations to be tracked for changes" + type = list(string) + default = [] +} + +variable "tag_names" { + description = "(Optional) A list of tag names for this workspace" + type = list(string) + default = [] +} + +variable "working_directory" { + description = "(Optional) A relative path that Terraform will execute within" + type = string + default = null +} + +variable "vcs_repository_identifier" { + description = "(Optional) A reference to your VCS repository in the format / where and refer to the organization and repository in your VCS provider. The format for Azure DevOps is //_git/" + type = string + default = "" +} + +variable "vcs_repository_branch" { + description = "(Optional) The repository branch that Terraform will execute from" + type = string + default = "" +} + +variable "vcs_repository_ingress_submodules" { + description = "(Optional) Whether submodules should be fetched when cloning the VCS repository" + type = bool + default = false +} + +variable "oauth_token_id" { + description = "(Optional) The token ID of the VCS connection to use" + type = string + default = "" +} + +variable "terraform_variables" { + description = <