Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,12 @@ If you have many remote repositories that you need to manage via this pattern, y
| <a name="input_gitlab"></a> [gitlab](#input\_gitlab) | The GitLab integration settings | <pre>object({<br/> namespace = string<br/> id = optional(string)<br/> })</pre> | `null` | no |
| <a name="input_labels"></a> [labels](#input\_labels) | List of labels to apply to the stacks. | `list(string)` | `[]` | no |
| <a name="input_manage_state"></a> [manage\_state](#input\_manage\_state) | Determines if Spacelift should manage state for this stack. | `bool` | `false` | no |
| <a name="input_project_root_prefix"></a> [project\_root\_prefix](#input\_project\_root\_prefix) | The path from the repository root to the root-modules directory. Used to set each stack's project\_root.<br/>This is the path Spacelift uses to find Terraform code in your repo.<br/><br/>When set, each stack's project\_root becomes: project\_root\_prefix/module\_name<br/>(e.g., "some-directory/root-modules" + "network" = "some-directory/root-modules/network")<br/><br/>Per-stack project\_root in YAML files takes precedence. | `string` | `null` | no |
| <a name="input_protect_from_deletion"></a> [protect\_from\_deletion](#input\_protect\_from\_deletion) | Protect this stack from accidental deletion. If set, attempts to delete this stack will fail. | `bool` | `false` | no |
| <a name="input_raw_git"></a> [raw\_git](#input\_raw\_git) | The raw Git integration settings | <pre>object({<br/> namespace = string<br/> url = string<br/> })</pre> | `null` | no |
| <a name="input_repository"></a> [repository](#input\_repository) | The name of your infrastructure repo | `string` | n/a | yes |
| <a name="input_root_module_structure"></a> [root\_module\_structure](#input\_root\_module\_structure) | The root module structure of the Stacks that you're reading in. See README for full details.<br/><br/>MultiInstance - You're using Workspaces or Dynamic Backend configuration to create multiple instances of the same root module code.<br/>SingleInstance - You're using copies of a root module and your directory structure to create multiple instances of the same Terraform code. | `string` | `"MultiInstance"` | no |
| <a name="input_root_modules_path"></a> [root\_modules\_path](#input\_root\_modules\_path) | The path, relative to the root of the repository, where the root module can be found. | `string` | `"root-modules"` | no |
| <a name="input_root_modules_path"></a> [root\_modules\_path](#input\_root\_modules\_path) | The path where root modules can be found, used internally by spacelift-automation to discover<br/>stack YAML files via fileset(). This path is relative to the spacelift-automation root module.<br/><br/>NOTE: It does NOT affect the configuration of created stacks (use project\_root\_prefix for that).<br/><br/>Example: If spacelift-automation is at `some-directory/root-modules/spacelift-automation/`<br/>and you want to discover stacks in sibling directories, use `../../root-modules`. | `string` | `"root-modules"` | no |
| <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 |
| <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 |
| <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 |
Expand Down
11 changes: 8 additions & 3 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,14 @@ locals {
local._multi_instance_structure ? "${module}-${trimsuffix(file, ".yaml")}" : module =>
merge(
{
# Use specified project_root, if not, build it using the root_modules_path and module name
"project_root" = try(content.stack_settings.project_root, replace(format("%s/%s", var.root_modules_path, module), "../", "")),
"root_module" = module,
# Use specified project_root, if not:
# - If project_root_prefix is set, use project_root_prefix/module
# - Otherwise, use root_modules_path/module with "../" stripped
"project_root" = try(
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I'm confused while reading this logic: Why is this necessary? if the only difference between using var.project_root_prefix and var.root_modules_path here is that we're replacing "../"... can we just pass var.project_root_prefix without the local directory paths? Does that not accomplish the same thing or am I missing something?

Copy link
Member

Choose a reason for hiding this comment

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

And let me clarify the above:

Can we just pass var.root_modules_path WITHOUT the ../../ and pass it as terraform/root-modules?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, we can't.

root_modules_path helps to discover sibling root modules and their stacks/*.yaml, while
project_root tells Spacelift where to find the Terraform code in the repo for each stack.

So from spacelift-automation/:

  • ../../root-modules → correctly finds network/, tfstate-backend/, etc. for discovery
  • but Spacelift needs terraform/root-modules/network (from repo root) to run the stack

project_root_prefix bridges this gap:

  • module discovers stacks using ../../root-modules
  • module sets each stack's project_root to terraform/root-modules/<module_name>

I think the root_modules_path name is a bit confusing here. I updated variables description to clarify the difference.

Copy link
Member

Choose a reason for hiding this comment

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

Hey @gberenice, alongside the upcoming major revision to fix stack naming that I'm asking to do as part of #101... I'm thinking that we introduce another variable here that provides what you're looking for and removes the usage of root module's path. If we do that as a breaking change, then we can keep this code cleaner while still providing the functionality that you need here.

What are your thoughts on that? Do you want to continue the conversation live?

content.stack_settings.project_root,
var.project_root_prefix != null ? format("%s/%s", var.project_root_prefix, module) : replace(format("%s/%s", var.root_modules_path, module), "../", "")
),
"root_module" = module,

# If default_tf_workspace_enabled is true, use "default" workspace, otherwise our file name is the workspace name
"terraform_workspace" = try(content.automation_settings.default_tf_workspace_enabled, local._default_tf_workspace_enabled) ? local.default_workspace_name : trimsuffix(file, ".yaml"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
kind: StackConfigV1
stack_settings:
# Custom project_root should take precedence over project_root_prefix
project_root: custom/path/to/root-module-a
labels:
- custom_project_root_label

automation_settings:
tfvars_enabled: false
151 changes: 151 additions & 0 deletions tests/project-root-prefix.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Tests for the project_root_prefix variable
# This variable allows setting a global prefix for project_root when root_modules_path
# uses relative paths for local scanning but the repo structure is different.

mock_provider "spacelift" {
mock_data "spacelift_spaces" {
defaults = {
spaces = []
}
}

mock_data "spacelift_worker_pools" {
defaults = {
worker_pools = []
}
}

mock_data "spacelift_aws_integrations" {
defaults = {
integrations = []
}
}
}

mock_provider "jsonschema" {
mock_data "jsonschema_validator" {
defaults = {
validated = "{}"
}
}
}

variables {
repository = "terraform-spacelift-automation"
github_enterprise = {
namespace = "masterpointio"
}
aws_integration_enabled = false
}

# Test default behavior without project_root_prefix (null)
# project_root should be calculated from root_modules_path with "../" stripped
run "test_project_root_without_prefix" {
command = plan

variables {
root_modules_path = "./tests/fixtures/multi-instance"
all_root_modules_enabled = true
}

# project_root should be based on root_modules_path
assert {
condition = spacelift_stack.default["root-module-a-test"].project_root == "./tests/fixtures/multi-instance/root-module-a"
error_message = "Project root without prefix should use root_modules_path: ${spacelift_stack.default["root-module-a-test"].project_root}"
}
}

# Test project_root_prefix with relative root_modules_path
# This is the main use case: local scanning with relative paths, but different repo structure
run "test_project_root_with_prefix" {
command = plan

variables {
root_modules_path = "./tests/fixtures/multi-instance"
project_root_prefix = "terraform/root-modules"
all_root_modules_enabled = true
}

# project_root should use the prefix instead of the stripped root_modules_path
assert {
condition = spacelift_stack.default["root-module-a-test"].project_root == "terraform/root-modules/root-module-a"
error_message = "Project root with prefix should use project_root_prefix: ${spacelift_stack.default["root-module-a-test"].project_root}"
}
}

# Test that project_root_prefix works with nested directories
run "test_project_root_prefix_with_nested_directories" {
command = plan

variables {
root_modules_path = "./tests/fixtures/nested-multi-instance"
project_root_prefix = "infra/terraform/modules"
root_module_structure = "MultiInstance"
all_root_modules_enabled = true
}

# project_root should preserve nested path with the prefix
assert {
condition = spacelift_stack.default["parent/nested-dev"].project_root == "infra/terraform/modules/parent/nested"
error_message = "Nested project_root with prefix incorrect: ${spacelift_stack.default["parent/nested-dev"].project_root}"
}
}

# Test that per-stack project_root in YAML takes precedence over project_root_prefix
run "test_stack_project_root_takes_precedence_over_prefix" {
command = plan

variables {
root_modules_path = "./tests/fixtures/multi-instance"
project_root_prefix = "should/be/ignored"
all_root_modules_enabled = true
}

# Stack with custom project_root in YAML should use that, not the prefix
assert {
condition = spacelift_stack.default["root-module-a-custom-project-root"].project_root == "custom/path/to/root-module-a"
error_message = "Per-stack project_root should take precedence over prefix: ${spacelift_stack.default["root-module-a-custom-project-root"].project_root}"
}

# Stack without custom project_root should use the prefix
assert {
condition = spacelift_stack.default["root-module-a-test"].project_root == "should/be/ignored/root-module-a"
error_message = "Stack without custom project_root should use prefix: ${spacelift_stack.default["root-module-a-test"].project_root}"
}
}

# Test that project_root_prefix works with SingleInstance structure
run "test_project_root_prefix_with_single_instance" {
command = plan

variables {
root_module_structure = "SingleInstance"
root_modules_path = "./tests/fixtures/single-instance"
project_root_prefix = "terraform/single"
all_root_modules_enabled = true
}

assert {
condition = spacelift_stack.default["root-module-a"].project_root == "terraform/single/root-module-a"
error_message = "SingleInstance project_root with prefix incorrect: ${spacelift_stack.default["root-module-a"].project_root}"
}
}

# Test that project_root_prefix replaces the fallback behavior that strips "../"
# This validates the main use case: root_modules_path with "../" for local scanning,
# project_root_prefix for the actual repo path
run "test_project_root_prefix_replaces_fallback" {
command = plan

variables {
root_modules_path = "./tests/fixtures/multi-instance"
project_root_prefix = "actual/repo/path"
all_root_modules_enabled = true
}

# With prefix set, the fallback logic is bypassed entirely
assert {
condition = spacelift_stack.default["root-module-a-test"].project_root == "actual/repo/path/root-module-a"
error_message = "Prefix should replace fallback entirely: ${spacelift_stack.default["root-module-a-test"].project_root}"
}
}
1 change: 1 addition & 0 deletions tests/resource-id-resolver.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mock_provider "spacelift" {
external_id = "test"
duration_seconds = 3600
generate_credentials_in_worker = false
autoattach_enabled = false
space_id = "root"
labels = []
region = "us-east-1"
Expand Down
2 changes: 1 addition & 1 deletion tests/schema-validation.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ run "test_stack_configs_schema_validation" {
}

assert {
condition = length(data.jsonschema_validator.stack_configs) == 8
condition = length(data.jsonschema_validator.stack_configs) == 9
error_message = "The fixture Stack Configs did not validate against the schema: ${jsonencode(data.jsonschema_validator.stack_configs)}"
}
}
24 changes: 23 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,32 @@ variable "branch" {

variable "root_modules_path" {
type = string
description = "The path, relative to the root of the repository, where the root module can be found."
description = <<-EOT
The path where root modules can be found, used internally by spacelift-automation to discover
stack YAML files via fileset(). This path is relative to the spacelift-automation root module.

NOTE: It does NOT affect the configuration of created stacks (use project_root_prefix for that).

Example: If spacelift-automation is at `some-directory/root-modules/spacelift-automation/`
and you want to discover stacks in sibling directories, use `../../root-modules`.
EOT
default = "root-modules"
}

variable "project_root_prefix" {
type = string
description = <<-EOT
The path from the repository root to the root-modules directory. Used to set each stack's project_root.
This is the path Spacelift uses to find Terraform code in your repo.

When set, each stack's project_root becomes: project_root_prefix/module_name
(e.g., "some-directory/root-modules" + "network" = "some-directory/root-modules/network")

Per-stack project_root in YAML files takes precedence.
EOT
default = null
}

variable "enabled_root_modules" {
type = list(string)
description = <<-EOT
Expand Down