Skip to content
Merged
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ If you have many remote repositories that you need to manage via this pattern, y
| [spacelift_stack.default](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/stack) | resource |
| [spacelift_stack_destructor.default](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/stack_destructor) | resource |
| [jsonschema_validator.runtime_overrides](https://registry.terraform.io/providers/bpedman/jsonschema/latest/docs/data-sources/validator) | data source |
| [spacelift_aws_integrations.all](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/aws_integrations) | data source |
| [spacelift_spaces.all](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/spaces) | data source |
| [spacelift_worker_pools.all](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/worker_pools) | data source |

Expand All @@ -347,6 +348,7 @@ If you have many remote repositories that you need to manage via this pattern, y
| <a name="input_aws_integration_attachment_write"></a> [aws\_integration\_attachment\_write](#input\_aws\_integration\_attachment\_write) | Indicates whether this attachment is used for write operations. | `bool` | `true` | no |
| <a name="input_aws_integration_enabled"></a> [aws\_integration\_enabled](#input\_aws\_integration\_enabled) | Indicates whether the AWS integration is enabled. | `bool` | `false` | no |
| <a name="input_aws_integration_id"></a> [aws\_integration\_id](#input\_aws\_integration\_id) | ID of the AWS integration to attach. | `string` | `null` | no |
| <a name="input_aws_integration_name"></a> [aws\_integration\_name](#input\_aws\_integration\_name) | Name of the AWS integration to attach, which will be resolved to aws\_integration\_id. | `string` | `null` | no |
| <a name="input_before_apply"></a> [before\_apply](#input\_before\_apply) | List of before-apply scripts | `list(string)` | `[]` | no |
| <a name="input_before_destroy"></a> [before\_destroy](#input\_before\_destroy) | List of before-destroy scripts | `list(string)` | `[]` | no |
| <a name="input_before_init"></a> [before\_init](#input\_before\_init) | List of before-init scripts | `list(string)` | `[]` | no |
Expand Down
5 changes: 2 additions & 3 deletions data.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Look up all spaces in order to map space names to space IDs
# Look up data sources in order to map [NAME] to [ID]
data "spacelift_spaces" "all" {}

# Look up all worker pools in order to map worker pool names to IDs
data "spacelift_worker_pools" "all" {}
data "spacelift_aws_integrations" "all" {}

# Validate the runtime overrides against the schema
# Frustrating that we have to do this, but this successfully validates the typing
Expand Down
89 changes: 56 additions & 33 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -288,20 +288,20 @@ locals {
))
}

## Handle space lookups

# Allow usage of space_name along with space_id.
# A space_id is long and hard to look at in the stack.yaml file, so pass in the space_name and it will be resolved to the space_id, which will be consumed by the `spacelife_stack` resource.
space_name_to_id = {
for space in data.spacelift_spaces.all.spaces :
space.name => space.space_id
}

## Handle worker pool names
# Allow usage of worker_pool_name along with worker_pool_id.
worker_pool_name_to_id = {
for pool in data.spacelift_worker_pools.all.worker_pools :
pool.name => pool.worker_pool_id
# Helper function to create name-to-ID mappings
name_to_id_mappings = {
space = {
for space in data.spacelift_spaces.all.spaces :
space.name => space.space_id
}
worker_pool = {
for pool in data.spacelift_worker_pools.all.worker_pools :
pool.name => pool.worker_pool_id
}
aws_integration = {
for integration in data.spacelift_aws_integrations.all.integrations :
integration.name => integration.integration_id
}
}

# Helper for property resolution with fallback to defaults
Expand All @@ -325,7 +325,7 @@ locals {
worker_pool_id = try(local.stack_configs[stack].worker_pool_id, var.worker_pool_id)

# AWS Integration properties
aws_integration_id = try(local.stack_configs[stack].aws_integration_id, var.aws_integration_id)
aws_integration_id = local.resolved_resource_ids.aws_integration[stack]

# Drift detection properties
drift_detection_ignore_state = try(local.stack_configs[stack].drift_detection_ignore_state, var.drift_detection_ignore_state)
Expand Down Expand Up @@ -354,24 +354,47 @@ locals {
}
}

resolved_space_ids = {
for stack in local.stacks : stack => coalesce(
try(local.stack_configs[stack].space_id, null), # space_id always takes precedence since it's the most explicit
try(local.space_name_to_id[local.stack_configs[stack].space_name], null), # Then try to look up space_name from the stack.yaml to ID
var.space_id,
try(local.space_name_to_id[var.space_name], null), # Then try to look up the space_name global variable to ID
local.root_space_id # If no space_id or space_name is provided, default to the root space
)
###############
# Resource Name to ID Resolver
#(e.g. space_name -> space_id so users can use the human readable name rather than the ID in configs)
###############
resource_resolver_config = {
space = {
id_attr = "space_id"
name_attr = "space_name"
default_value = local.root_space_id
}
worker_pool = {
id_attr = "worker_pool_id"
name_attr = "worker_pool_name"
default_value = null
}
aws_integration = {
id_attr = "aws_integration_id"
name_attr = "aws_integration_name"
default_value = null
}
}

# Resolve worker_pool_id if worker_pool_name is provided
resolved_worker_pool_ids = {
for stack in local.stacks : stack => try(coalesce(
try(local.stack_configs[stack].worker_pool_id, null), # worker_pool_id always takes precedence since it's the most explicit
try(local.worker_pool_name_to_id[local.stack_configs[stack].worker_pool_name], null), # Then try to look up worker_pool_name from the stack.yaml to ID
var.worker_pool_id, # Then try to use the global variable worker_pool_id
try(local.worker_pool_name_to_id[var.worker_pool_name], null), # Then try to look up the global variable worker_pool_name to ID
), null) # If no worker_pool_id or worker_pool_name is provided, default to null
var_lookup = { # We need this map to dynamically access vars like var.space_id when config.id_attr = "space_id". TF doesn't support var[dynamic_key] syntax, downside of it not being a full programming language.
space_id = var.space_id
space_name = var.space_name
worker_pool_id = var.worker_pool_id
worker_pool_name = var.worker_pool_name
aws_integration_id = var.aws_integration_id
aws_integration_name = var.aws_integration_name
}

# Resolve Precedence order: stack ID > stack name > global ID > global name > default
resolved_resource_ids = {
for resource_type, config in local.resource_resolver_config : resource_type => {
for stack in local.stacks : stack => try(coalesce(
try(local.stack_configs[stack][config.id_attr], null), # Direct stack-level ID always takes precedence
try(local.name_to_id_mappings[resource_type][local.stack_configs[stack][config.name_attr]], null), # Direct stack-level name resolution
local.var_lookup[config.id_attr], # Global variable ID
try(local.name_to_id_mappings[resource_type][local.var_lookup[config.name_attr]], null), # Global variable name resolution
), config.default_value) # Resource-specific default
}
}

## Filter integration + drift detection stacks
Expand Down Expand Up @@ -447,12 +470,12 @@ resource "spacelift_stack" "default" {
protect_from_deletion = local.stack_property_resolver[each.key].protect_from_deletion
repository = local.stack_property_resolver[each.key].repository
runner_image = local.stack_property_resolver[each.key].runner_image
space_id = local.resolved_space_ids[each.key]
space_id = local.resolved_resource_ids.space[each.key]
terraform_smart_sanitization = local.stack_property_resolver[each.key].terraform_smart_sanitization
terraform_version = local.stack_property_resolver[each.key].terraform_version
terraform_workflow_tool = var.terraform_workflow_tool
terraform_workspace = local.configs[each.key].terraform_workspace
worker_pool_id = local.resolved_worker_pool_ids[each.key]
worker_pool_id = local.resolved_resource_ids.worker_pool[each.key]

# Usage of `templatestring` requires OpenTofu 1.7 and Terraform 1.9 or later.
description = coalesce(
Expand Down
4 changes: 4 additions & 0 deletions stack-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@
"type": "string",
"description": "AWS integration ID"
},
"aws_integration_name": {
"type": "string",
"description": "AWS integration name, this will be translated to an aws_integration_id. Mutually exclusive with aws_integration_id"
},
"drift_detection_enabled": {
"type": "boolean",
"description": "Whether to enable drift detection"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ kind: StackConfigV1
stack_settings:
space_id: direct-space-id-stack-yaml # Tests direct space_id precedence over global variable space_id
worker_pool_name: mp-ue1-automation-spft-priv-workers # Tests worker_pool_name gets translated to worker_pool_id
aws_integration_name: mp-automation-755965222190 # Tests aws_integration_name gets translated to aws_integration_id
labels:
- test_label
40 changes: 32 additions & 8 deletions tests/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -848,8 +848,8 @@ run "test_space_name_resolves_to_correct_id" {
command = plan

assert {
condition = local.resolved_space_ids["root-module-a-default-example"] == "mp-aws-automation-01JK7A21DW1YH3Q64JHS3RYNP9" # For the `masterpointio.app.spacelift.io`
error_message = "Space name not resolving to correct ID: ${jsonencode(local.resolved_space_ids)}"
condition = local.resolved_resource_ids.space["root-module-a-default-example"] == "mp-aws-automation-01JK7A21DW1YH3Q64JHS3RYNP9" # For the `masterpointio.app.spacelift.io`
error_message = "Space name not resolving to correct ID: ${jsonencode(local.resolved_resource_ids.space)}"
}
}

Expand All @@ -862,8 +862,8 @@ run "test_space_id_takes_precedence_over_space_id_global_variable" {
}

assert {
condition = local.resolved_space_ids["root-module-a-test"] == "direct-space-id-stack-yaml"
error_message = "Space ID from stack settings not taking precedence over global variable space ID: ${jsonencode(local.resolved_space_ids)}"
condition = local.resolved_resource_ids.space["root-module-a-test"] == "direct-space-id-stack-yaml"
error_message = "Space ID from stack settings not taking precedence over global variable space ID: ${jsonencode(local.resolved_resource_ids.space)}"
}
}

Expand Down Expand Up @@ -1009,8 +1009,8 @@ run "test_worker_pool_name_resolves_to_correct_id" {
command = plan

assert {
condition = local.resolved_worker_pool_ids["root-module-a-test"] == "01K3VABYB4FBXNV24KN4A4EKC8" # For the `mp-ue1-automation-spft-priv-workers` in our `mp-infra` Spacelift account
error_message = "Worker pool name not resolving to correct ID: ${jsonencode(local.resolved_worker_pool_ids)}"
condition = local.resolved_resource_ids.worker_pool["root-module-a-test"] == "01K3VABYB4FBXNV24KN4A4EKC8" # For the `mp-ue1-automation-spft-priv-workers` in our `mp-infra` Spacelift account
error_message = "Worker pool name not resolving to correct ID: ${jsonencode(local.resolved_resource_ids.worker_pool)}"
}
}

Expand All @@ -1023,7 +1023,31 @@ run "test_worker_pool_name_takes_precedence_over_worker_pool_name_global_variabl
}

assert {
condition = local.resolved_worker_pool_ids["root-module-a-test"] == "01K3VABYB4FBXNV24KN4A4EKC8" # For the `mp-ue1-automation-spft-priv-workers` in our `mp-infra` Spacelift account
error_message = "Worker pool name from stack settings not taking precedence over global variable worker_pool_name: ${jsonencode(local.resolved_worker_pool_ids)}"
condition = local.resolved_resource_ids.worker_pool["root-module-a-test"] == "01K3VABYB4FBXNV24KN4A4EKC8" # For the `mp-ue1-automation-spft-priv-workers` in our `mp-infra` Spacelift account
error_message = "Worker pool name from stack settings not taking precedence over global variable worker_pool_name: ${jsonencode(local.resolved_resource_ids.worker_pool)}"
}
}

# Test that aws_integration_name from stack settings resolves to correct ID
run "test_aws_integration_name_resolves_to_correct_id" {
command = plan

assert {
condition = local.resolved_resource_ids.aws_integration["root-module-a-test"] == "01JEC7ZACVKHTSVY4NF8QNZVVB" # For the `mp-automation-755965222190` in our `mp-infra` Spacelift account
error_message = "AWS integration name not resolving to correct ID: ${jsonencode(local.resolved_resource_ids.aws_integration)}"
}
}

# Test that aws_integration_name from stack settings takes precedence over aws_integration_name global variable
run "test_aws_integration_name_takes_precedence_over_aws_integration_name_global_variable" {
command = plan

variables {
aws_integration_name = "some-other-aws-integration"
}

assert {
condition = local.resolved_resource_ids.aws_integration["root-module-a-test"] == "01JEC7ZACVKHTSVY4NF8QNZVVB" # For the `mp-automation-755965222190` in our `mp-infra` Spacelift account
error_message = "AWS integration name from stack settings not taking precedence over global variable aws_integration_name: ${jsonencode(local.resolved_resource_ids.aws_integration)}"
}
}
8 changes: 4 additions & 4 deletions tests/single-instance.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ run "test_default_space_id_is_used" {
command = plan

assert {
condition = local.resolved_space_ids["root-module-a"] == "root"
error_message = "Default space_id (root) was not used when no other values provided: ${jsonencode(local.resolved_space_ids)}"
condition = local.resolved_resource_ids.space["root-module-a"] == "root"
error_message = "Default space_id (root) was not used when no other values provided: ${jsonencode(local.resolved_resource_ids.space)}"
}
}

Expand All @@ -155,7 +155,7 @@ run "test_space_id_is_used_from_stack_yaml" {
command = plan

assert {
condition = local.resolved_space_ids["root-module-b"] == "some-space-id"
error_message = "Space ID from stack.yaml is not being used: ${jsonencode(local.resolved_space_ids)}"
condition = local.resolved_resource_ids.space["root-module-b"] == "some-space-id"
error_message = "Space ID from stack.yaml is not being used: ${jsonencode(local.resolved_resource_ids.space)}"
}
}
6 changes: 6 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ variable "aws_integration_id" {
default = null
}

variable "aws_integration_name" {
type = string
description = "Name of the AWS integration to attach, which will be resolved to aws_integration_id."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add something like "we recommend using name instead of ID for clarity/readability."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Added that, also the fact that names must be unique in Spacelift so users don't have to worry about duplicated ones!

default = null
}

variable "aws_integration_attachment_read" {
type = bool
description = "Indicates whether this attachment is used for read operations."
Expand Down