Skip to content

Commit 8143a92

Browse files
authored
feat: ability to assign AWS integration by name instead of ID only, and add TF Check blocks for mutual exclusivity (#92)
Translate and resolve AWS integration names to ID value. Even though this is more lines of code (because of Terraform programming limitation), I believe this is more readable and maintainable since we avoid having duplicated logic for all values we want to resolve names -> IDs. We now have 3 resources that resolves names into ID, but in the future, we might have more resources that follow similar patterns (e.g. Azure integration ID, etc.) This will allow a more human readable name in the stack configs rather than ugly IDs <img width="400" height="500" alt="image" src="https://github.com/user-attachments/assets/19adae87-31a2-4c2d-b3b3-27b283557205" /> All tests are passing without changing the logic, showing that it is working as intended. I will follow up with another PR to organize the tests in a separate file dedicated to resolving names -> IDs since the main.tftest.hcl is getting out of hand. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Added support for specifying AWS integrations by name via aws_integration_name in module inputs and stack configuration; names are automatically resolved to IDs with clear precedence rules. - Unified ID resolution now covers spaces, worker pools, and AWS integrations for more consistent configuration. - Documentation - Updated README to document the new aws_integration_name input and the Spacelift AWS integrations data source, including usage details. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 0392abc commit 8143a92

File tree

9 files changed

+162
-59
lines changed

9 files changed

+162
-59
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ If you have many remote repositories that you need to manage via this pattern, y
325325
| [spacelift_stack.default](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/stack) | resource |
326326
| [spacelift_stack_destructor.default](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/stack_destructor) | resource |
327327
| [jsonschema_validator.runtime_overrides](https://registry.terraform.io/providers/bpedman/jsonschema/latest/docs/data-sources/validator) | data source |
328+
| [spacelift_aws_integrations.all](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/aws_integrations) | data source |
328329
| [spacelift_spaces.all](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/spaces) | data source |
329330
| [spacelift_worker_pools.all](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/worker_pools) | data source |
330331

@@ -347,6 +348,7 @@ If you have many remote repositories that you need to manage via this pattern, y
347348
| <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 |
348349
| <a name="input_aws_integration_enabled"></a> [aws\_integration\_enabled](#input\_aws\_integration\_enabled) | Indicates whether the AWS integration is enabled. | `bool` | `false` | no |
349350
| <a name="input_aws_integration_id"></a> [aws\_integration\_id](#input\_aws\_integration\_id) | ID of the AWS integration to attach. | `string` | `null` | no |
351+
| <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. We recommend using names rather than IDs to improve clarity & readability. Since Spacelift enforces unique names, you can rely on names as identifiers without worrying about duplication issues. | `string` | `null` | no |
350352
| <a name="input_azure_devops"></a> [azure\_devops](#input\_azure\_devops) | The Azure DevOps integration settings | <pre>object({<br/> project = string<br/> id = optional(string)<br/> })</pre> | `null` | no |
351353
| <a name="input_before_apply"></a> [before\_apply](#input\_before\_apply) | List of before-apply scripts | `list(string)` | `[]` | no |
352354
| <a name="input_before_destroy"></a> [before\_destroy](#input\_before\_destroy) | List of before-destroy scripts | `list(string)` | `[]` | no |
@@ -379,12 +381,12 @@ If you have many remote repositories that you need to manage via this pattern, y
379381
| <a name="input_runner_image"></a> [runner\_image](#input\_runner\_image) | URL of the Docker image used to process Runs. Defaults to `null` which is Spacelift's standard (Alpine) runner image. | `string` | `null` | no |
380382
| <a name="input_runtime_overrides"></a> [runtime\_overrides](#input\_runtime\_overrides) | Runtime overrides that are merged into the stack config.<br/> This allows for per-root-module overrides of the stack resources at runtime<br/> so you have more flexibility beyond the variable defaults and the static stack config files.<br/> Keys are the root module names and values match the StackConfig schema.<br/> See `stack-config.schema.json` for full details on the schema and<br/> `tests/fixtures/multi-instance/root-module-a/stacks/default-example.yaml` for a complete example. | `any` | `{}` | no |
381383
| <a name="input_space_id"></a> [space\_id](#input\_space\_id) | Place the created stacks in the specified space\_id. Mutually exclusive with space\_name. | `string` | `null` | no |
382-
| <a name="input_space_name"></a> [space\_name](#input\_space\_name) | Place the created stacks in the specified space\_name. Mutually exclusive with space\_id. | `string` | `null` | no |
384+
| <a name="input_space_name"></a> [space\_name](#input\_space\_name) | Place the created stacks in the specified space\_name. Mutually exclusive with space\_id. We recommend using names rather than IDs to improve clarity & readability. Since Spacelift enforces unique names, you can rely on names as identifiers without worrying about duplication issues. | `string` | `null` | no |
383385
| <a name="input_spaces"></a> [spaces](#input\_spaces) | A map of Spacelift Spaces to create | <pre>map(object({<br/> description = optional(string, null)<br/> inherit_entities = optional(bool, false)<br/> labels = optional(list(string), null)<br/> parent_space_id = optional(string, "root")<br/> }))</pre> | `{}` | no |
384386
| <a name="input_terraform_smart_sanitization"></a> [terraform\_smart\_sanitization](#input\_terraform\_smart\_sanitization) | Indicates whether runs on this will use terraform's sensitive value system to sanitize<br/>the outputs of Terraform state and plans in spacelift instead of sanitizing all fields. | `bool` | `false` | no |
385387
| <a name="input_terraform_version"></a> [terraform\_version](#input\_terraform\_version) | OpenTofu/Terraform version to use. Defaults to the latest available version of the `terraform_workflow_tool`. | `string` | `null` | no |
386388
| <a name="input_terraform_workflow_tool"></a> [terraform\_workflow\_tool](#input\_terraform\_workflow\_tool) | Defines the tool that will be used to execute the workflow.<br/>This can be one of OPEN\_TOFU, TERRAFORM\_FOSS or CUSTOM. | `string` | `"OPEN_TOFU"` | no |
387-
| <a name="input_worker_pool_id"></a> [worker\_pool\_id](#input\_worker\_pool\_id) | ID of the worker pool to use. Mutually exclusive with worker\_pool\_name.<br/>NOTE: worker\_pool\_name or worker\_pool\_id is required when using a self-hosted instance of Spacelift. | `string` | `null` | no |
389+
| <a name="input_worker_pool_id"></a> [worker\_pool\_id](#input\_worker\_pool\_id) | ID of the worker pool to use. Mutually exclusive with worker\_pool\_name.<br/>NOTE: worker\_pool\_name or worker\_pool\_id is required when using a self-hosted instance of Spacelift.<br/>We recommend using names rather than IDs to improve clarity & readability. Since Spacelift enforces unique names, you can rely on names as identifiers without worrying about duplication issues. | `string` | `null` | no |
388390
| <a name="input_worker_pool_name"></a> [worker\_pool\_name](#input\_worker\_pool\_name) | Name of the worker pool to use. Mutually exclusive with worker\_pool\_id.<br/>NOTE: worker\_pool\_name or worker\_pool\_id is required when using a self-hosted instance of Spacelift. | `string` | `null` | no |
389391

390392
## Outputs

checks.tf

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* These check blocks assertions enforce mutual exclusivity between ID and name variables.
3+
*
4+
* It took a bit for me to understand the logic, so here's an explanation (with the help with AI):
5+
* The condition "var.x_id == null || var.x_name == null" is an ASSERTION that must be TRUE.
6+
* It asserts: "space_id must be null OR space_name must be null" (at least one must be null).
7+
*
8+
* When both are set: space_id is NOT null AND space_name is NOT null
9+
* → false || false = false → ASSERTION FAILS → TF fails (so BOTH shouldn't be set at the same time)
10+
*
11+
* When only one is set: one is null, one is not null
12+
* → true || false = true → ASSERTION PASSES → TF continues (so one can be set, the other can be null)
13+
*
14+
* Truth Table:
15+
* | x_id | x_name | Condition Result | TF Action |
16+
* |-----------|-------------|-------------------------|------------------|
17+
* | null | null | true || true = true | ✅ PASS |
18+
* | null | "some-name" | true || false = true | ✅ PASS |
19+
* | "some-id" | null | false || true = true | ✅ PASS |
20+
* | "some-id" | "some-name" | false || false = false | ❌ FAIL |
21+
*/
22+
23+
check "spaces_enforce_exclusivity" {
24+
assert {
25+
condition = var.space_id == null || var.space_name == null
26+
error_message = "space_id and space_name are mutually exclusive."
27+
}
28+
}
29+
30+
check "worker_pools_mutual_exclusivity" {
31+
assert {
32+
condition = var.worker_pool_id == null || var.worker_pool_name == null
33+
error_message = "worker_pool_id and worker_pool_name are mutually exclusive."
34+
}
35+
}
36+
37+
check "aws_integrations_mutual_exclusivity" {
38+
assert {
39+
condition = var.aws_integration_id == null || var.aws_integration_name == null
40+
error_message = "aws_integration_id and aws_integration_name are mutually exclusive."
41+
}
42+
}

data.tf

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
# Look up all spaces in order to map space names to space IDs
1+
# Look up data sources in order to map [NAME] to [ID]
22
data "spacelift_spaces" "all" {}
3-
4-
# Look up all worker pools in order to map worker pool names to IDs
53
data "spacelift_worker_pools" "all" {}
4+
data "spacelift_aws_integrations" "all" {}
65

76
# Validate the runtime overrides against the schema
87
# Frustrating that we have to do this, but this successfully validates the typing

main.tf

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -288,22 +288,6 @@ locals {
288288
))
289289
}
290290

291-
## Handle space lookups
292-
293-
# Allow usage of space_name along with space_id.
294-
# 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.
295-
space_name_to_id = {
296-
for space in data.spacelift_spaces.all.spaces :
297-
space.name => space.space_id
298-
}
299-
300-
## Handle worker pool names
301-
# Allow usage of worker_pool_name along with worker_pool_id.
302-
worker_pool_name_to_id = {
303-
for pool in data.spacelift_worker_pools.all.worker_pools :
304-
pool.name => pool.worker_pool_id
305-
}
306-
307291
# Helper for property resolution with fallback to defaults
308292
stack_property_resolver = {
309293
for stack in local.stacks : stack => {
@@ -325,7 +309,7 @@ locals {
325309
worker_pool_id = try(local.stack_configs[stack].worker_pool_id, var.worker_pool_id)
326310

327311
# AWS Integration properties
328-
aws_integration_id = try(local.stack_configs[stack].aws_integration_id, var.aws_integration_id)
312+
aws_integration_id = local.resource_id_resolver.aws_integration[stack]
329313

330314
# Drift detection properties
331315
drift_detection_ignore_state = try(local.stack_configs[stack].drift_detection_ignore_state, var.drift_detection_ignore_state)
@@ -354,24 +338,70 @@ locals {
354338
}
355339
}
356340

357-
resolved_space_ids = {
358-
for stack in local.stacks : stack => coalesce(
359-
try(local.stack_configs[stack].space_id, null), # space_id always takes precedence since it's the most explicit
360-
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
361-
var.space_id,
362-
try(local.space_name_to_id[var.space_name], null), # Then try to look up the space_name global variable to ID
363-
local.root_space_id # If no space_id or space_name is provided, default to the root space
364-
)
341+
###############
342+
# Resource Name to ID Resolver
343+
#(e.g. space_name -> space_id so users can use the human readable name rather than the ID in configs)
344+
###############
345+
name_to_id_mappings = {
346+
space = {
347+
for space in data.spacelift_spaces.all.spaces :
348+
space.name => space.space_id
349+
}
350+
worker_pool = {
351+
for pool in data.spacelift_worker_pools.all.worker_pools :
352+
pool.name => pool.worker_pool_id
353+
}
354+
aws_integration = {
355+
for integration in data.spacelift_aws_integrations.all.integrations :
356+
integration.name => integration.integration_id
357+
}
358+
}
359+
360+
resource_id_resolver_config = {
361+
space = {
362+
id_attr = "space_id"
363+
name_attr = "space_name"
364+
default_value = local.root_space_id
365+
}
366+
worker_pool = {
367+
id_attr = "worker_pool_id"
368+
name_attr = "worker_pool_name"
369+
default_value = null
370+
}
371+
aws_integration = {
372+
id_attr = "aws_integration_id"
373+
name_attr = "aws_integration_name"
374+
default_value = null
375+
}
365376
}
366377

367-
# Resolve worker_pool_id if worker_pool_name is provided
368-
resolved_worker_pool_ids = {
369-
for stack in local.stacks : stack => try(coalesce(
370-
try(local.stack_configs[stack].worker_pool_id, null), # worker_pool_id always takes precedence since it's the most explicit
371-
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
372-
var.worker_pool_id, # Then try to use the global variable worker_pool_id
373-
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
374-
), null) # If no worker_pool_id or worker_pool_name is provided, default to null
378+
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.
379+
space_id = var.space_id
380+
space_name = var.space_name
381+
worker_pool_id = var.worker_pool_id
382+
worker_pool_name = var.worker_pool_name
383+
aws_integration_id = var.aws_integration_id
384+
aws_integration_name = var.aws_integration_name
385+
}
386+
387+
# How it works:
388+
# 1. Loops through each resource type (space, worker_pool, aws_integration)
389+
# 2. For each stack, tries to resolve the ID using coalesce() with this precedence: stack ID > stack name > global ID > global name > default
390+
# Example for space resolution on stack "my-stack":
391+
# 1. Check local.stack_configs["my-stack"]["space_id"] (direct ID from YAML)
392+
# 2. Check local.name_to_id_mappings["space"][local.stack_configs["my-stack"]["space_name"]] (name→ID from YAML)
393+
# 3. Check var.space_id (global module variable)
394+
# 4. Check local.name_to_id_mappings["space"][var.space_name] (global name→ID)
395+
# 5. Fall back to local.root_space_id ("root")
396+
resource_id_resolver = {
397+
for resource_type, config in local.resource_id_resolver_config : resource_type => {
398+
for stack in local.stacks : stack => try(coalesce(
399+
try(local.stack_configs[stack][config.id_attr], null), # Direct stack-level ID always takes precedence
400+
try(local.name_to_id_mappings[resource_type][local.stack_configs[stack][config.name_attr]], null), # Direct stack-level name resolution
401+
local.var_lookup[config.id_attr], # Global variable ID
402+
try(local.name_to_id_mappings[resource_type][local.var_lookup[config.name_attr]], null), # Global variable name resolution
403+
), config.default_value) # Resource-specific default
404+
}
375405
}
376406

377407
## Filter integration + drift detection stacks
@@ -390,12 +420,6 @@ locals {
390420
}
391421
}
392422

393-
check "spaces_enforce_mutual_exclusivity" {
394-
assert {
395-
condition = var.space_id == null || var.space_name == null
396-
error_message = "space_id and space_name are mutually exclusive."
397-
}
398-
}
399423

400424
# Perform deep merge for common configurations and stack configurations
401425
module "deep" {
@@ -447,12 +471,12 @@ resource "spacelift_stack" "default" {
447471
protect_from_deletion = local.stack_property_resolver[each.key].protect_from_deletion
448472
repository = local.stack_property_resolver[each.key].repository
449473
runner_image = local.stack_property_resolver[each.key].runner_image
450-
space_id = local.resolved_space_ids[each.key]
474+
space_id = local.resource_id_resolver.space[each.key]
451475
terraform_smart_sanitization = local.stack_property_resolver[each.key].terraform_smart_sanitization
452476
terraform_version = local.stack_property_resolver[each.key].terraform_version
453477
terraform_workflow_tool = var.terraform_workflow_tool
454478
terraform_workspace = local.configs[each.key].terraform_workspace
455-
worker_pool_id = local.resolved_worker_pool_ids[each.key]
479+
worker_pool_id = local.resource_id_resolver.worker_pool[each.key]
456480

457481
# Usage of `templatestring` requires OpenTofu 1.7 and Terraform 1.9 or later.
458482
description = coalesce(

stack-config.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@
209209
"type": "string",
210210
"description": "AWS integration ID"
211211
},
212+
"aws_integration_name": {
213+
"type": "string",
214+
"description": "AWS integration name, this will be translated to an aws_integration_id. Mutually exclusive with aws_integration_id"
215+
},
212216
"drift_detection_enabled": {
213217
"type": "boolean",
214218
"description": "Whether to enable drift detection"

tests/fixtures/multi-instance/root-module-a/stacks/test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ kind: StackConfigV1
22
stack_settings:
33
space_id: direct-space-id-stack-yaml # Tests direct space_id precedence over global variable space_id
44
worker_pool_name: mp-ue1-automation-spft-priv-workers # Tests worker_pool_name gets translated to worker_pool_id
5+
aws_integration_name: mp-automation-755965222190 # Tests aws_integration_name gets translated to aws_integration_id
56
labels:
67
- test_label

0 commit comments

Comments
 (0)