From 0f6c9189541eb81341ce4faf9a596accb220e947 Mon Sep 17 00:00:00 2001 From: Veronika Gnilitska Date: Sat, 26 Oct 2024 16:56:20 -0400 Subject: [PATCH] chore: cleanup --- README.md | 30 +- examples/complete/README.md | 83 --- .../components/random-pet/stacks/common.yaml | 5 + .../components/random-pet/stacks/example.yaml | 4 +- .../random-pet/tfvars/example.tfvars | 1 + .../components/random-pet/variables.tf | 1 - .../spacelift-policies/.terraform.lock.hcl | 34 -- .../components/spacelift-policies/README.md | 7 - .../components/spacelift-policies/context.tf | 168 ------ .../components/spacelift-policies/main.tf | 18 - .../spacelift-policies/providers.tf | 1 - .../spacelift-policies/stacks/common.yaml | 8 - .../stacks/trigger-retries.yaml | 3 - .../tfvars/trigger-retries.tfvars | 5 - .../spacelift-policies/variables.tf | 39 -- .../components/spacelift-policies/versions.tf | 10 - examples/complete/context.tf | 169 ++++-- examples/complete/default.auto.tfvars | 1 - examples/complete/example.tfvars | 78 --- ...fault.auto.tfvars => fixtures.auto.tfvars} | 0 examples/complete/main.tf | 481 +----------------- examples/complete/outputs.tf | 1 - examples/complete/providers.tf | 7 +- examples/complete/variables.tf | 247 --------- .../spacelift-automation/.terraform.lock.hcl | 77 --- modules/spacelift-automation/README.md | 181 +++++-- modules/spacelift-automation/context.tf | 169 ++++-- .../spacelift-automation/default.auto.tfvars | 14 - modules/spacelift-automation/main.tf | 312 ++---------- modules/spacelift-automation/providers.tf | 15 +- modules/spacelift-automation/refactor.tf | 45 -- modules/spacelift-automation/variables.tf | 43 +- modules/spacelift-automation/versions.tf | 4 +- 33 files changed, 511 insertions(+), 1750 deletions(-) delete mode 100644 examples/complete/README.md create mode 100644 examples/complete/components/random-pet/stacks/common.yaml create mode 100644 examples/complete/components/random-pet/tfvars/example.tfvars delete mode 100644 examples/complete/components/spacelift-policies/.terraform.lock.hcl delete mode 100644 examples/complete/components/spacelift-policies/README.md delete mode 100644 examples/complete/components/spacelift-policies/context.tf delete mode 100644 examples/complete/components/spacelift-policies/main.tf delete mode 100644 examples/complete/components/spacelift-policies/providers.tf delete mode 100644 examples/complete/components/spacelift-policies/stacks/common.yaml delete mode 100644 examples/complete/components/spacelift-policies/stacks/trigger-retries.yaml delete mode 100644 examples/complete/components/spacelift-policies/tfvars/trigger-retries.tfvars delete mode 100644 examples/complete/components/spacelift-policies/variables.tf delete mode 100644 examples/complete/components/spacelift-policies/versions.tf delete mode 100644 examples/complete/default.auto.tfvars delete mode 100644 examples/complete/example.tfvars rename examples/complete/{components/spacelift-policies/default.auto.tfvars => fixtures.auto.tfvars} (100%) delete mode 100644 examples/complete/outputs.tf delete mode 100644 examples/complete/variables.tf delete mode 100644 modules/spacelift-automation/.terraform.lock.hcl delete mode 100644 modules/spacelift-automation/default.auto.tfvars delete mode 100644 modules/spacelift-automation/refactor.tf diff --git a/README.md b/README.md index 865dbf2..5abec59 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,8 @@ -# terraform-module-template +# terraform-spacelift-automation -[![Release](https://img.shields.io/github/release/masterpointio/terraform-module-template.svg)](https://github.com/masterpointio/terraform-module-template/releases/latest) +[![Release](https://img.shields.io/github/release/masterpointio/terraform-spacelift-automation.svg)](https://github.com/masterpointio/terraform-spacelift-automation/releases/latest) -This repository serves as a template for creating Terraform modules, providing a standardized structure and essential files for efficient module development. It's designed to ensure consistency and best practices across Terraform projects. - -## Usage - -TODO - - - -## Requirements - -No requirements. - -## Providers - -No provider. - -## Inputs - -No input. - -## Outputs - -No output. - - +This Terraform module collections provides infrastructure automation for projects in [Spacelift](https://docs.spacelift.io/). ## Contributing diff --git a/examples/complete/README.md b/examples/complete/README.md deleted file mode 100644 index c79084a..0000000 --- a/examples/complete/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# `spacelift-automation` - -This Terraform root module provides infrastructure automation for Masterpoint's internal projects in [Spacelift](https://docs.spacelift.io/). - -## Overview - -The `spacelift-automation` root module is designed to streamline the deployment and management of all Spacelift infrastructure, including itself. - -At the moment we have one common [spacelift-automation-mp-main](https://masterpointio.app.spacelift.io/stack/spacelift-automation-mp-main) stack that manages all the clients and internal stacks. - -It automates the creation of "child" stacks and all the required accompanying Spacelift resources. For each root module configured in the `root-modules/spacelift-automation/tfvars/mp-automation.tfvars` it creates: - -1. Spacelift [Stack](https://docs.spacelift.io/concepts/stack/) - You can think about a stack as a combination of source code, state file and configuration in the form of environment variables and mounted files. -1. Spacelift [Stack Destructor](https://docs.spacelift.io/concepts/stack/stack-dependencies.html#ordered-stack-creation-and-deletion) - Required to destroy the resources of a Stack before deleting it. Destroying this resource will delete the resources in the stack. If this resource needs to be deleted and the resources in the stacks are to be preserved, ensure that the deactivated attribute is set to true. -1. Spacelift [AWS Role](https://docs.spacelift.io/integrations/cloud-providers/aws#lets-explain) - Represents cross-account IAM role delegation between the Spacelift worker and an individual stack or module. -1. Spacelift [Mounted File](https://docs.spacelift.io/concepts/configuration/environment.html#mounted-files) - This feature allows to manage the OpenTofu variables by mounting corresponding tfvars files into a working directory during the run and inject variable definitions into the OpenTofu execution environment. - -These files are automatically mounted and available during the OpenTofu plan and apply stages. - -⚠️ `spacelift-automation-mp-main` manages all the mounted files and, hence, all the tfvars attached to the stacks. It's crucial to understand that a commit must kick the `spacelift-automation-mp-main` stack first to update all the necessary mounted files, and the dependent stacks run will be started. This flow is managed with Push and Trigger Spacelift Policies. Our custom Policies are stored in [config/spacelift-policies](../../config/spacelift-policies/) directory. - -## Usage - -Due to the project specifics, Spacelift Automation logic heavily relies on the Git repository structure. -The root module `spacelift-automation` is configured to track all the files in the provided root module directory and create the stack based on the provided configuration (if any). - -Let's check the example. -Input repo structure: - -``` -├── root-modules -│ ├── spacelift-aws-role -│ │ ├── tfvars -│ │ │ └── automation.tfvars -│ │ │ └── dev.tfvars -│ │ ├── variables.tf -│ │ └── versions.tf -│ └── spacelift-spaces -│ └── tfvars -│ └── mp-automation.tfvars -... -``` - -Root module inputs: - -```hcl -aws_role_arn = "arn:aws:iam::755965222190:role/Spacelift" - -# GitHub configuration -github_enterprise = { - namespace = "masterpointio" -} -repository = "spacelift-certification" - -# Stacks configurations -root_modules = { - spacelift-aws-role = { - common_stack_configs = { - autodeploy = true - } - stacks = { - automation = { - description = "The AWS IAM role to assume and put its temporary credentials in `masterpoint-automation` in the runtime environment." - space_id = "mp-automation-01J2BSSM6TW46GJ6EJNZW1WP2B" - } - } - } - spacelift-spaces = {} -} - -``` - -The configuration above creates the following stacks: - -- `spacelift-aws-role-automation` with the overridden `autodeploy`, `space_id` and `description` values -- `spacelift-aws-role-dev` with the overriden `autodeploy` valuee -- `spacelift-spaces-mp-automation` with the default stack values - -Corresponding Terraform variables are mounted to each stack. For example, the content of the file `root-modules/spacelift-policies/tfvars/push-default.tfvars` will be mounted to the stack `spacelift-policies-push-default`, allowing the Terraform inputs to be provided to the configuration. diff --git a/examples/complete/components/random-pet/stacks/common.yaml b/examples/complete/components/random-pet/stacks/common.yaml new file mode 100644 index 0000000..d455e8e --- /dev/null +++ b/examples/complete/components/random-pet/stacks/common.yaml @@ -0,0 +1,5 @@ +stack_settings: + manage_state: true + description: This stack generates random pet names +tfvars: + enabled: false diff --git a/examples/complete/components/random-pet/stacks/example.yaml b/examples/complete/components/random-pet/stacks/example.yaml index bffdd01..38a4c49 100644 --- a/examples/complete/components/random-pet/stacks/example.yaml +++ b/examples/complete/components/random-pet/stacks/example.yaml @@ -1,6 +1,4 @@ stack_settings: manage_state: true tfvars: - enabled: false - - + enabled: true diff --git a/examples/complete/components/random-pet/tfvars/example.tfvars b/examples/complete/components/random-pet/tfvars/example.tfvars new file mode 100644 index 0000000..983ef02 --- /dev/null +++ b/examples/complete/components/random-pet/tfvars/example.tfvars @@ -0,0 +1 @@ +length = 10 diff --git a/examples/complete/components/random-pet/variables.tf b/examples/complete/components/random-pet/variables.tf index 6348a57..e44654e 100644 --- a/examples/complete/components/random-pet/variables.tf +++ b/examples/complete/components/random-pet/variables.tf @@ -1,5 +1,4 @@ variable "length" { description = "The length of the random name" type = number - default = 2 } diff --git a/examples/complete/components/spacelift-policies/.terraform.lock.hcl b/examples/complete/components/spacelift-policies/.terraform.lock.hcl deleted file mode 100644 index 19b5961..0000000 --- a/examples/complete/components/spacelift-policies/.terraform.lock.hcl +++ /dev/null @@ -1,34 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/http" { - version = "3.4.3" - constraints = ">= 3.0.0" - hashes = [ - "h1:eyxO2EduIJQT3f2k7a9PDG/ldwMxf6VkOKb0jlvrMLc=", - "zh:00947c294bd98c05ad99b9757b3fcf73588d8e7717d978c54752a3bf6e192e65", - "zh:14d6c167b2bc38cd5c4fa97ba64ebae6f00b4820cfcf74731e80b7b511833db2", - "zh:1cbb9ba5651f353d781d18ef699acf9c462a4837ba24ac74f40f03accd9de15a", - "zh:304f736067b01cd34badd436a974836e466c4f3ee82d9715d91d734c6a5f7f3c", - "zh:67639a4269db17f953135d686b6b4fc8043374e39eade832d57c971be93af1b1", - "zh:6772970cd2ecc4a2bbefc7c8cf4b3d87075c704b0b038b618ae831f7c9a3f04a", - "zh:80d4189289835319133450ce12838775c8ffdd7bfd53b4262f5c4f8ebb0dc17f", - "zh:8b1c691b2e764273abe0b29832d675cbe31fbe04634a6a83eba003d60aafa535", - "zh:8db396edaf793b627369dbada055ba9a9e364e46d21ca0215fe8685740927f75", - "zh:b2536ea0b1b390365781e102abcaa75f38973c2a7345bca8ae5000a7fdfb7e3b", - ] -} - -provider "registry.opentofu.org/spacelift-io/spacelift" { - version = "1.14.0" - constraints = ">= 0.1.31, ~> 1.0" - hashes = [ - "h1:Jy4qMbZWTUnjD7c5pLOijUdv+HUSRcES1XhDSREdhxw=", - "zh:8f73b3772298c39d9aa2beb733c37060e3ab022a09f826b7c0ab376c2100f5e9", - "zh:a835aed9f7d5b01b58f0c33a53925b138456c7e351d683d53b541bfce820e5d4", - "zh:c1744901e2704bceb75592dae6d6fe6eab7c91e6668e20279b1f22d495a63222", - "zh:c822f2494cdceaa848b46dd603f4c4523032d6777d4796409125f06e7383c02d", - "zh:e224cc87104b819effb75ecc149fdc7e991f43eac1ae071488891148c75e2150", - "zh:e5d87534455f95868030b9b30918d01cb4da131055d54af7da48cf53c5baa35b", - ] -} diff --git a/examples/complete/components/spacelift-policies/README.md b/examples/complete/components/spacelift-policies/README.md deleted file mode 100644 index 3739243..0000000 --- a/examples/complete/components/spacelift-policies/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# `spacelift-policies` - -This root module is responsible for managing the Spacelift Policies as code. - -A Spacelift Policy is a set of rules and conditions defined to manage and control the behavior of infrastructure as code (IaC) workflows within the Spacelift platform. Spacelift policies are written using the Rego language, which is part of the Open Policy Agent (OPA) framework. These policies can enforce security, compliance, and operational best practices, ensuring that infrastructure changes adhere to organizational standards. - -See the official [Spacelift Policy documentaion](https://docs.spacelift.io/concepts/policy/). diff --git a/examples/complete/components/spacelift-policies/context.tf b/examples/complete/components/spacelift-policies/context.tf deleted file mode 100644 index 8b703cd..0000000 --- a/examples/complete/components/spacelift-policies/context.tf +++ /dev/null @@ -1,168 +0,0 @@ -# -# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label -# All other instances of this file should be a copy of that one -# -# -# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf -# and then place it in your Terraform module to automatically get -# Cloud Posse's standard configuration inputs suitable for passing -# to Cloud Posse modules. -# -# Modules should access the whole context as `module.this.context` -# to get the input variables with nulls for defaults, -# for example `context = module.this.context`, -# and access individual variables as `module.this.`, -# with final values filled in. -# -# For example, when using defaults, `module.this.context.delimiter` -# will be null, and `module.this.delimiter` will be `-` (hyphen). -# - -module "this" { - source = "cloudposse/label/null" - version = "0.24.1" - - enabled = var.enabled - namespace = var.namespace - environment = var.environment - stage = var.stage - name = var.name - delimiter = var.delimiter - attributes = var.attributes - tags = var.tags - additional_tag_map = var.additional_tag_map - label_order = var.label_order - regex_replace_chars = var.regex_replace_chars - id_length_limit = var.id_length_limit - - context = var.context -} - -# Copy contents of cloudposse/terraform-null-label/variables.tf here - -variable "context" { - type = object({ - enabled = bool - namespace = string - environment = string - stage = string - name = string - delimiter = string - attributes = list(string) - tags = map(string) - additional_tag_map = map(string) - regex_replace_chars = string - label_order = list(string) - id_length_limit = number - }) - default = { - enabled = true - namespace = null - environment = null - stage = null - name = null - delimiter = null - attributes = [] - tags = {} - additional_tag_map = {} - regex_replace_chars = null - label_order = [] - id_length_limit = null - } - description = <<-EOT - Single object for setting entire context at once. - See description of individual variables for details. - Leave string and numeric variables as `null` to use default value. - Individual variable settings (non-null) override settings in context object, - except for attributes, tags, and additional_tag_map, which are merged. - EOT -} - -variable "enabled" { - type = bool - default = null - description = "Set to false to prevent the module from creating any resources" -} - -variable "namespace" { - type = string - default = null - description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" -} - -variable "environment" { - type = string - default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" -} - -variable "stage" { - type = string - default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" -} - -variable "name" { - type = string - default = null - description = "Solution name, e.g. 'app' or 'jenkins'" -} - -variable "delimiter" { - type = string - default = null - description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. - Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. - EOT -} - -variable "attributes" { - type = list(string) - default = [] - description = "Additional attributes (e.g. `1`)" -} - -variable "tags" { - type = map(string) - default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" -} - -variable "additional_tag_map" { - type = map(string) - default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." -} - -variable "label_order" { - type = list(string) - default = null - description = <<-EOT - The naming order of the id output and Name tag. - Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, but at least one must be present. - EOT -} - -variable "regex_replace_chars" { - type = string - default = null - description = <<-EOT - Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. - If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. - EOT -} - -variable "id_length_limit" { - type = number - default = null - description = <<-EOT - Limit `id` to this many characters. - Set to `0` for unlimited length. - Set to `null` for default, which is `0`. - Does not affect `id_full`. - EOT -} - -#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/complete/components/spacelift-policies/main.tf b/examples/complete/components/spacelift-policies/main.tf deleted file mode 100644 index 816a090..0000000 --- a/examples/complete/components/spacelift-policies/main.tf +++ /dev/null @@ -1,18 +0,0 @@ -locals { - enabled = module.this.enabled -} - -module "policy" { - source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy" - version = "1.6.0" - - count = local.enabled ? 1 : 0 - - policy_name = module.this.name - body = try(file(var.body_path), null) - body_url = var.body_url - body_url_version = var.body_url_version - type = var.type - labels = var.labels - space_id = var.space_id -} diff --git a/examples/complete/components/spacelift-policies/providers.tf b/examples/complete/components/spacelift-policies/providers.tf deleted file mode 100644 index c95d538..0000000 --- a/examples/complete/components/spacelift-policies/providers.tf +++ /dev/null @@ -1 +0,0 @@ -provider "spacelift" {} diff --git a/examples/complete/components/spacelift-policies/stacks/common.yaml b/examples/complete/components/spacelift-policies/stacks/common.yaml deleted file mode 100644 index a904c59..0000000 --- a/examples/complete/components/spacelift-policies/stacks/common.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Common settings for all stacks -tfvars: - enabled: true -stack_settings: - administrative: true - manage_state: true - - diff --git a/examples/complete/components/spacelift-policies/stacks/trigger-retries.yaml b/examples/complete/components/spacelift-policies/stacks/trigger-retries.yaml deleted file mode 100644 index 7a99e99..0000000 --- a/examples/complete/components/spacelift-policies/stacks/trigger-retries.yaml +++ /dev/null @@ -1,3 +0,0 @@ -stack_settings: - autodeploy: true - description: TRIGGER policy that allows automatically restarting the failed run diff --git a/examples/complete/components/spacelift-policies/tfvars/trigger-retries.tfvars b/examples/complete/components/spacelift-policies/tfvars/trigger-retries.tfvars deleted file mode 100644 index f0041ad..0000000 --- a/examples/complete/components/spacelift-policies/tfvars/trigger-retries.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -name = "trigger-retries" -type = "TRIGGER" -body_url = "https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/trigger.retries.rego" -body_url_version = "1.6.0" -labels = ["autoattach:administrative"] diff --git a/examples/complete/components/spacelift-policies/variables.tf b/examples/complete/components/spacelift-policies/variables.tf deleted file mode 100644 index ef74d17..0000000 --- a/examples/complete/components/spacelift-policies/variables.tf +++ /dev/null @@ -1,39 +0,0 @@ -variable "body_path" { - type = string - description = "The path to the file that contains the body of the policy to create. Mutually exclusive with `var.body_url`" - default = null -} - -variable "body_url" { - type = string - description = "The URL of file containing the body of policy to create. Mutually exclusive with `var.body_path`." - default = null -} - -variable "body_url_version" { - type = string - description = "The optional policy version injected using a %s in `var.body_url`. This can be pinned to a version tag or a branch." - default = "main" -} - -variable "type" { - type = string - description = "The type of the policy to create." - - validation { - condition = can(regex("^(ACCESS|APPROVAL|GIT_PUSH|INITIALIZATION|LOGIN|PLAN|TASK|TRIGGER|NOTIFICATION)$", var.type)) - error_message = "The type must be one of ACCESS, APPROVAL, GIT_PUSH, INITIALIZATION, LOGIN, PLAN, TASK, TRIGGER or NOTIFICATION" - } -} - -variable "labels" { - type = list(string) - description = "List of labels to add to the policy." - default = null -} - -variable "space_id" { - type = string - description = "The `space_id` (slug) of the space the policy is in." - default = "root" -} diff --git a/examples/complete/components/spacelift-policies/versions.tf b/examples/complete/components/spacelift-policies/versions.tf deleted file mode 100644 index 7416ee4..0000000 --- a/examples/complete/components/spacelift-policies/versions.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = "~> 1.6" - - required_providers { - spacelift = { - source = "spacelift-io/spacelift" - version = "~> 1.0" - } - } -} diff --git a/examples/complete/context.tf b/examples/complete/context.tf index 8b703cd..5e0ef88 100644 --- a/examples/complete/context.tf +++ b/examples/complete/context.tf @@ -8,6 +8,8 @@ # Cloud Posse's standard configuration inputs suitable for passing # to Cloud Posse modules. # +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# # Modules should access the whole context as `module.this.context` # to get the input variables with nulls for defaults, # for example `context = module.this.context`, @@ -20,10 +22,11 @@ module "this" { source = "cloudposse/label/null" - version = "0.24.1" + version = "0.25.0" # requires Terraform >= 0.13.0 enabled = var.enabled namespace = var.namespace + tenant = var.tenant environment = var.environment stage = var.stage name = var.name @@ -34,6 +37,10 @@ module "this" { label_order = var.label_order regex_replace_chars = var.regex_replace_chars id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags context = var.context } @@ -41,23 +48,11 @@ module "this" { # Copy contents of cloudposse/terraform-null-label/variables.tf here variable "context" { - type = object({ - enabled = bool - namespace = string - environment = string - stage = string - name = string - delimiter = string - attributes = list(string) - tags = map(string) - additional_tag_map = map(string) - regex_replace_chars = string - label_order = list(string) - id_length_limit = number - }) + type = any default = { enabled = true namespace = null + tenant = null environment = null stage = null name = null @@ -68,6 +63,17 @@ variable "context" { regex_replace_chars = null label_order = [] id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] } description = <<-EOT Single object for setting entire context at once. @@ -76,6 +82,16 @@ variable "context" { Individual variable settings (non-null) override settings in context object, except for attributes, tags, and additional_tag_map, which are merged. EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } } variable "enabled" { @@ -87,32 +103,42 @@ variable "enabled" { variable "namespace" { type = string default = null - description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" } variable "environment" { type = string default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" } variable "stage" { type = string default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" } variable "name" { type = string default = null - description = "Solution name, e.g. 'app' or 'jenkins'" + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT } variable "delimiter" { type = string default = null description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. + Delimiter to be used between ID elements. Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. EOT } @@ -120,36 +146,64 @@ variable "delimiter" { variable "attributes" { type = list(string) default = [] - description = "Additional attributes (e.g. `1`)" + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT } variable "tags" { type = map(string) default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT } variable "additional_tag_map" { type = map(string) default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT } variable "label_order" { type = list(string) default = null description = <<-EOT - The naming order of the id output and Name tag. + The order in which the labels (ID elements) appear in the `id`. Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, but at least one must be present. - EOT + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT } variable "regex_replace_chars" { type = string default = null description = <<-EOT - Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. EOT } @@ -158,11 +212,68 @@ variable "id_length_limit" { type = number default = null description = <<-EOT - Limit `id` to this many characters. + Limit `id` to this many characters (minimum 6). Set to `0` for unlimited length. - Set to `null` for default, which is `0`. + Set to `null` for keep the existing setting, which defaults to `0`. Does not affect `id_full`. EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT } #### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/complete/default.auto.tfvars b/examples/complete/default.auto.tfvars deleted file mode 100644 index 3477e9e..0000000 --- a/examples/complete/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = true \ No newline at end of file diff --git a/examples/complete/example.tfvars b/examples/complete/example.tfvars deleted file mode 100644 index 14903b9..0000000 --- a/examples/complete/example.tfvars +++ /dev/null @@ -1,78 +0,0 @@ -aws_role_arn = "arn:aws:iam::755965222190:role/Spacelift" - -# GitHub configuration -github_enterprise = { - namespace = "masterpointio" -} -repository = "spacelift-certification" - -# Stacks configurations -root_modules = { - spacelift-aws-role = { - stacks = { - automation = { - description = "The AWS IAM role to assume and put its temporary credentials in `masterpoint-automation` in the runtime environment." - space_id = "mp-automation-01J2BSSM6TW46GJ6EJNZW1WP2B" - } - } - } - spacelift-spaces = { - stacks = { - mp-automation = { - administrative = true - description = "Spacelift space for all non-administrative resources for masterpoint-automation." - } - } - } - spacelift-automation = { - stacks = { - mp-main = { - administrative = true - description = "Administrative Spacelift Stack for managing Masterpoint's infrastructure for internal projects." - } - } - } - spacelift-policies = { - common_stack_configs = { - administrative = true - } - stacks = { - push-default = { - description = "Default Push Policy." - } - trigger-administrative = { - description = "Policy to trigger the stack after it gets created in the `administrative` stack." - } - trigger-dependencies = { - description = "Policy to trigger other stacks that depend on the current stack based on the label `depends-on:`" - } - plan-iam-policy-modify = { - description = "Plan policy used to raise a warning for any modification made to aws_iam_policy resources." - } - approval-iam-policy-modify = { - description = "Approval policy used by Security team to approve aws_iam_policy resource changes. Conditionally dependent on the plan-iam-policy-modify Plan policy." - } - } - } - kms-key = { - common_stack_configs = { - space_id = "mp-automation-01J2BSSM6TW46GJ6EJNZW1WP2B" - } - # stacks = { - # sops-key = { - # space_id = "mp-automation-01J2BSSM6TW46GJ6EJNZW1WP2B" - # description = "The key to encrypt/decrypt SOPS secrets." - # } - # } - } - spacelift-blueprint = { - common_stack_configs = { - administrative = true - } - stacks = { - aws-integration = { - description = "Blueprint to create stacks for AWS Integration." - } - } - } -} diff --git a/examples/complete/components/spacelift-policies/default.auto.tfvars b/examples/complete/fixtures.auto.tfvars similarity index 100% rename from examples/complete/components/spacelift-policies/default.auto.tfvars rename to examples/complete/fixtures.auto.tfvars diff --git a/examples/complete/main.tf b/examples/complete/main.tf index da451fe..3215281 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,483 +1,12 @@ -# locals { -# root_modules_path = "components" -# enabled_root_modules = ["spacelift-policies"] #, "spacelift-automation"] - -# # Read and decode stack YAML files from the root directory -# root_module_yaml_decoded = { -# for module in local.enabled_root_modules : module => { -# for yaml_file in fileset("${path.root}/${local.root_modules_path}/${module}/stacks", "*.yaml") : -# yaml_file => yamldecode(file("${path.root}/${local.root_modules_path}/${module}/stacks/${yaml_file}")) -# } -# } - -# # Retrieve common stack configurations for each root module -# common_content = { -# for module, file in local.root_module_yaml_decoded : module => lookup(file, "common.yaml", {}) -# } - -# # Merge all stack configurations from the root modules into a single map -# stacks_content = merge([for module, files in local.root_module_yaml_decoded : { -# for file, content in files : "${module}-${trimsuffix(file, ".yaml")}" => -# merge( -# { "root_module" = module }, -# content -# ) if file != "common.yaml" -# } -# ]...) -# } - -# # Merge stack configurations with the common configurations -# module "deepmerge" { -# source = "cloudposse/config/yaml//modules/deepmerge" -# version = "1.0.2" -# for_each = local.stacks_content -# maps = [each.value, local.common_content[each.value.root_module]] -# } - module "automation" { - source = "../../modules/spacelift-automation/" - #aws_role_arn = "arn:aws:iam::755965222190:role/Spacelift" # Need to replace with AWS integration + source = "../../modules/spacelift-automation/" github_enterprise = { namespace = "masterpointio" } repository = "terraform-spacelift-automation" - root_modules_path = "components" - enabled_root_modules = ["spacelift-policies"] - aws_integration_id = "01J30JBKQTCD72ATZCRWHYST3C" + branch = "feature/initial-version" # TODO: remove this + root_modules_path = "../../examples/complete/components" + enabled_root_modules = ["random-pet"] + aws_integration_id = "01J30JBKQTCD72ATZCRWHYST3C" } -# # This Terraform code automates the creation and management of Spacelift stacks based on the structure -# # and configurations defined in the Git repository, default Stack values and additional input variables. -# # It primarily relies on dynamic local expressions to generate configurations based on the -# # input variables and Git structure so it can be consumed by Spacelift resources. -# # This module can also manage the automation stack intself, but it should be bootsrapped manually. -# # -# # It handles the following: -# # 1. Workspaces (see ## Workspaces) -# # Reads workspace names for each root module set in the root_modules variable in the spacelift-automation -# # Workspace names are extracted from the files in each root module `tfvars` directory, and are equal to the file names. -# # Stacks names are build from the root module name and workspace name. -# # -# # 2. Stack Configurations from Git (see ## Stack Configurations from Git) -# # Reads the Spacelift stack configurations strictly based on the root modules structure in Git and file names. -# # These are the configurations required to be set for a stack, e.g. project_root, terraform_workspace, root_module. -# # -# # 3. Stack Configurations from Terraform variables (see ## Stack Configurations from Terraform variables) -# # Reads the Spacelift stack configurations explicitly specified in the spacelift-automation tfvars file. -# # These configurations are intended to override the default values. -# # -# # 4. Common Stack configrations -# # Some configurations are euqal across the whole root module, and can be set it on a root module level: -# # * Space IDs: in the majority if cases all the workspaces in a root module belong to the same Spacelift space, so -# # we allow setting a "global" space_id for all stacks on a root module level. -# # * Autodeploy: if all the stacks in a root module should be autodeployed. -# # * Administrative: if all the stacks in a root module are administrative, e.g stacks that manage Spacelift resources. -# # -# # 5. Dependencies (see ## Dependencies) -# # Builds stack dependencies based on the root modules configuration. -# # Our main case is to support the dependency from each child from its parent stack, which is spacelift-automation-. -# # -# # 6. Labels (see ## Labels) -# # Generates labels for the stacks based on administrative, dependency, and folder information. -# # -# # Syntax note: -# # The local expression started with an underscore `_` is used to store intermediate values -# # that are not directly used in the resource creation. - -# locals { -# enabled = module.this.enabled -# aws_role_enabled = local.enabled && var.aws_role_enabled - -# ## Workspaces -# # Extracts the list of workspace names from tfvars files for each given root module. -# # Root module name is used as a key of the map. -# # For root_modules = { -# # "client-infra" = {}, -# # "spacelift-automation" = { -# # stacks = { -# # mp-automation = { -# # administrative = true -# # description = "Administrative Spacelift Stack for managing Masterpoint's Infrastructure." -# # } -# # } -# # } -# # } -# # If the tfvars directory for `client-infra` contains files "client1.tfvars" and "client2.tfvars", -# # and for `spacelift-automation` it contains "mp-automation.tfvars": -# # The resulting workspaces will be: -# # { -# # "client-infra" = [ -# # "client1", -# # "client2", -# # ] -# # "spacelift-automation" = [ -# # "mp-automation", -# # ] -# # } - -# _workspaces = { -# for key in keys(var.root_modules) : key => [ -# for file in fileset(format("../../%s/%s/tfvars/", var.root_modules_path, key), "*.tfvars") : -# trimsuffix(file, ".tfvars") -# ] -# } - -# ## Stack Configurations from Git -# # Creates a map of the Git based stack configurations for each root module. -# # Example for workspaces `client1` and `client2` in `client-infra` root module: -# # { -# # "client-infra" = { -# # "client-infra-client1" = { -# # "terraform_workspace" = "client1" -# # "project_root" = "root-modules/client-infra" -# # "root_module" = "client-infra" -# # } -# # "client-infra-client2" = { -# # "terraform_workspace" = "client2" -# # "project_root" = "root-modules/client-infra" -# # "root_module" = "client-infra" -# # } -# # } -# # } - -# _git_stack_configs = { -# for module, workspaces in local._workspaces : module => { -# for workspace in workspaces : "${module}-${workspace}" => -# { -# terraform_workspace = workspace, -# project_root = format("%s/%s", var.root_modules_path, module) -# root_module = module -# } -# } -# } - -# # Flatten the stack configurations based into a single map. -# # Example for the `client-infra` root module: -# # { -# # "client-infra-client1" = { -# # "terraform_workspace" = "client1" -# # "project_root" = "root-modules/client-infra" -# # "root_module" = "client-infra" -# # } -# # "client-infra-client2" = { -# # "terraform_workspace" = "client2" -# # "project_root" = "root-modules/client-infra" -# # "root_module" = "client-infra" -# # } -# # } - -# _flat_git_stack_configs = merge([ -# for module, stacks in local._git_stack_configs : { -# for stack_name, stack in stacks : stack_name => stack -# } -# ]...) - -# ## Stack Configurations from Terraform variables -# # Creates a map of the stack configurations that should be created for each root module specified in -# # the spacelift-automation tfvars file. -# # Example for workspaces `trigger-automated-retries` and `trigger-dependencies` in `spacelift-policies` root module: -# # { -# # "spacelift-policies" = { -# # "spacelift-policies-trigger-automated-retries" = { -# # "administrative" = true -# # } -# # "spacelift-policies-trigger-dependencies" = { -# # "administrative" = true -# # "description" = "Policy to trigger other stacks." -# # } -# # } - -# _tfvars_stack_configs = { -# for module_name, configs in var.root_modules : module_name => { -# for workspace_name, workspace_configs in coalesce(configs.stacks, {}) : "${module_name}-${workspace_name}" => workspace_configs -# } -# } - -# # Flatten the configured stacks into a single map and merge with the common stack configurations. -# # Example for the `spacelift-policies` root module: -# # { -# # "spacelift-policies-trigger-automated-retries" = { -# # "administrative" = true -# # } -# # "spacelift-policies-trigger-dependencies" = { -# # "administrative" = true -# # "description" = "Policy to trigger other stacks." -# # } -# # } - -# _flat_tfvars_stack_configs = merge([ -# for module, stack in local._tfvars_stack_configs : { -# for stack_name, stack_config in stack : stack_name => stack_config -# } -# ]...) - -# _common_stack_configs = { -# for module, stacks in local._git_stack_configs : module => { -# for stack_name, stack in stacks : stack_name => try(var.root_modules[module].common_stack_configs, {}) -# } -# } - -# _flat_common_stack_configs = merge([ -# for module, stack in local._common_stack_configs : { -# for stack_name, stack_config in stack : stack_name => stack_config -# } -# ]...) - -# # Iterate over stack configs in git and merge all stack configurations from tfvars into a single map. -# stack_configs = { -# for k, v in local._flat_git_stack_configs : k => merge( -# v, -# try(local._flat_common_stack_configs[k], {}), -# try(local._flat_tfvars_stack_configs[k], {}), -# ) -# } - -# # Get the list of all stack names -# stacks = toset(keys(local.stack_configs)) - -# ## Dependencies - -# # Get the dependencies from the root modules configuration. -# # Expected to be set on a per-module. Might want to revisit this in the future. -# # Child stacks always depend on the spacelift-automation stack. -# # Example for the `client-infra` root module: -# # { -# # "client-infra" = { -# # "depends_on_stack_ids" = tolist([ -# # "spacelift-automation-mp-main", -# # "spacelift-webhooks-slack-notifications", -# # ]) -# # } -# # } - -# _module_depends_on_stack_ids = { -# for module_name, module in var.root_modules : module_name => { -# depends_on_stack_ids = [format("%s-%s", "spacelift-automation", terraform.workspace)] -# } -# } -# # Creates a map of the dependencies list based for each stack. -# # Example for the `client-infra` root module: -# # { -# # "client-infra-client1" = { -# # depends_on_stack_ids = ["spacelift-automation-mp-main", "spacelift-webhooks-slack-notifications"] -# # } -# # "client-infra-client2" = { -# # depends_on_stack_ids = ["spacelift-automation-mp-main", "spacelift-webhooks-slack-notifications"] -# # } -# # } - -# _depends_on_stack_ids = merge([ -# for module, stacks in local._git_stack_configs : { -# for stack_name, stack in stacks : stack_name => { -# depends_on_stack_ids = concat( -# values(lookup(local._module_depends_on_stack_ids, module, [])), -# try(local.stack_configs[stacks.stack_name].depends_on_stack_ids, []) -# ) -# } -# } -# ]...) - -# # Break dependencies into a flat list. -# # Example for the `client-infra` root module: -# # [ -# # { -# # stack_id = "client-infra-client1" -# # depends_on_stack_id = "spacelift-automation-mp-main" -# # }, -# # { -# # stack_id = "client-infra-client1" -# # depends_on_stack_id = "spacelift-webhooks-slack-notifications" -# # }, -# # { -# # stack_id = "client-infra-client2" -# # depends_on_stack_id = "spacelift-automation-mp-main" -# # }, -# # { -# # stack_id = "client-infra-client2" -# # depends_on_stack_id = "spacelift-webhooks-slack-notifications" -# # } -# # ] - -# _dependency_list = flatten([ -# for stack_name, config in local._depends_on_stack_ids : [ -# for depends_on in config.depends_on_stack_ids : [ -# for id in depends_on : { -# stack_id = stack_name -# depends_on_stack_id = id -# } -# ] -# ] -# ]) - -# ## Labels -# # Сreates a map of administrative labels for each stack that has the administrative property set to true. -# # Example: -# # { -# # "spacelift-automation-mp-main" = [ -# # "administrative", -# # ] -# # "spacelift-policies-notify-stack-creation" = [ -# # "administrative", -# # ] -# # "spacelift-policies-notify-tf-completed" = [ -# # "administrative", -# # ] -# # } - -# _administrative_labels = { -# # Normalizing the value here is required due to TF not enforcing -# # strict type-checking for `stacks = optional(any)` in var.root_modules -# for stack_name, stack in local.stack_configs : stack_name => ["administrative"] if tobool(try(stack.administrative, false)) == true -# } - -# # Creates a map of `depends-on` labels for each stack based on the root module level dependency configuration. -# # Example: -# # { -# # "spacelift-spaces-clients" = [ -# # "depends-on:spacelift-automation-mp-main", -# # ] -# # "spacelift-webhooks-notify-tf-completed" = [ -# # "depends-on:spacelift-automation-mp-main", -# # ] -# # } - -# _dependency_labels = { -# for dependency in local._dependency_list : -# dependency.stack_id => [ -# "depends-on:${dependency.depends_on_stack_id}" -# ] -# } - -# # Creates a map of folder labels for each stack based on Git structure for a proper grouping stacks in Spacelift UI. -# # https://docs.spacelift.io/concepts/stack/organizing-stacks#label-based-folders -# # Example: -# # { -# # "spacelift-spaces-clients" = [ -# # "folder:spacelift-spaces/clients", -# # ] -# # "spacelift-spaces-prod" = [ -# # "folder:spacelift-spaces/prod", -# # ] -# # "spacelift-webhooks-notify-tf-completed" = [ -# # "folder:spacelift-webhooks/notify-tf-completed", -# # ] -# # } - -# _folder_labels = merge([ -# for module, stacks in local._git_stack_configs : { -# for stack_name, stack in stacks : stack_name => [ -# "folder:${stack.root_module}/${stack.terraform_workspace}" -# ] -# } -# ]...) - -# # Merge all labels into a single map for each stack. -# # Example: -# # { -# # "spacelift-spaces-clients" = [ -# # "folder:spacelift-spaces/clients", -# # "depends-on:spacelift-automation-mp-main", -# # ] -# # } - -# labels = { -# for stack in local.stacks : -# stack => compact(flatten([ -# lookup(local._administrative_labels, stack, []), -# lookup(local._folder_labels, stack, []), -# lookup(local._dependency_labels, stack, []), -# try(local.stack_configs[stack].labels, []), -# ])) -# } - -# # Merge all before_init steps into a single map for each stack. -# before_init = { -# for stack in local.stacks : stack => compact(concat( -# var.stack_before_init, -# try(local.stack_configs[stack].before_init, []), -# # This command is required for each stack. -# # It copies the tfvars file from the stack's workspace to the root module's directory -# # and renames it to `spacelift.auto.tfvars` to automatically load variable definitions for each run/task. -# ["cp tfvars/${local.stack_configs[stack].terraform_workspace}.tfvars spacelift.auto.tfvars"], -# )) -# } -# } - -# resource "spacelift_stack" "this" { -# for_each = local.enabled ? local.stacks : toset([]) - -# space_id = coalesce(try(local.stack_configs[each.key].space_id, null), var.stack_space_id) -# name = each.key -# administrative = coalesce(try(local.stack_configs[each.key].administrative, null), var.stack_administrative) -# after_apply = compact(concat(try(local.stack_configs[each.key].after_apply, []), var.stack_after_apply)) -# after_destroy = compact(concat(try(local.stack_configs[each.key].after_destroy, []), var.stack_after_destroy)) -# after_init = compact(concat(try(local.stack_configs[each.key].after_init, []), var.stack_after_init)) -# after_perform = compact(concat(try(local.stack_configs[each.key].after_perform, []), var.stack_after_perform)) -# after_plan = compact(concat(try(local.stack_configs[each.key].after_plan, []), var.stack_after_plan)) -# autodeploy = coalesce(try(local.stack_configs[each.key].autodeploy, null), var.stack_autodeploy) -# autoretry = try(local.stack_configs[each.key].autoretry, var.stack_autoretry) -# before_apply = compact(coalesce(try(local.stack_configs[each.key].before_apply, []), var.stack_before_apply)) -# before_destroy = compact(coalesce(try(local.stack_configs[each.key].before_destroy, []), var.stack_before_destroy)) -# before_init = local.before_init[each.key] -# before_perform = compact(coalesce(try(local.stack_configs[each.key].before_perform, []), var.stack_before_perform)) -# before_plan = compact(coalesce(try(local.stack_configs[each.key].before_plan, []), var.stack_before_plan)) -# description = coalesce(try(local.stack_configs[each.key].description, null), var.stack_description) -# repository = try(local.stack_configs[each.key].repository, var.repository) -# branch = try(local.stack_configs[each.key].branch, var.branch) -# project_root = local.stack_configs[each.key].project_root -# manage_state = try(local.stack_configs[each.key].manage_state, var.stack_manage_state) -# labels = local.labels[each.key] -# enable_local_preview = try(local.stack_configs[each.key].enable_local_preview, var.stack_enable_local_preview) -# terraform_smart_sanitization = try(local.stack_configs[each.key].terraform_smart_sanitization, var.stack_terraform_smart_sanitization) -# terraform_version = try(local.stack_configs[each.key].terraform_version, var.stack_terraform_version) -# terraform_workflow_tool = var.terraform_workflow_tool -# terraform_workspace = local.stack_configs[each.key].terraform_workspace - -# protect_from_deletion = try(local.stack_configs[each.key].protect_from_deletion, var.stack_protect_from_deletion) - -# worker_pool_id = try(local.stack_configs[each.key].worker_pool_id, var.stack_worker_pool_id) - -# dynamic "github_enterprise" { -# for_each = var.github_enterprise != null ? [var.github_enterprise] : [] -# content { -# namespace = github_enterprise.value["namespace"] -# } -# } -# } - -# # The Spacelift Destructor is a feature designed to automatically clean up the resources no longer managed by our IaC. -# # Don't toggle the creation/destruction of this resource with var.stack_destructor_enabled, -# # as it will delete all resources in the stack when toggled from 'true' to 'false'. -# # Use the 'deactivated' attribute to disable the stack destructor functionality instead. -# # https://github.com/spacelift-io/terraform-provider-spacelift/blob/master/spacelift/resource_stack_destructor.go -# resource "spacelift_stack_destructor" "this" { -# for_each = local.enabled ? local.stacks : toset([]) - -# stack_id = spacelift_stack.this[each.key].id -# deactivated = !try(local.stack_configs[each.key].destructor_enabled, var.stack_destructor_enabled) - -# depends_on = [ -# spacelift_drift_detection.this, -# spacelift_aws_role.this -# ] -# } - -# resource "spacelift_aws_role" "this" { -# for_each = local.aws_role_enabled ? local.stacks : toset([]) - -# stack_id = spacelift_stack.this[each.key].id -# role_arn = var.aws_role_arn -# } - -# resource "spacelift_drift_detection" "this" { -# for_each = local.enabled ? { -# for key, value in local.stacks : key => value -# if try(local.stack_configs[key].drift_detection_enabled, var.stack_drift_detection_enabled) -# } : {} - -# stack_id = spacelift_stack.this[each.key].id -# ignore_state = try(local.stack_configs[each.key].drift_detection_ignore_state, var.stack_drift_detection_ignore_state) -# reconcile = try(local.stack_configs[each.key].drift_detection_reconcile, var.stack_drift_detection_reconcile) -# schedule = try(local.stack_configs[each.key].drift_detection_schedule, var.stack_drift_detection_schedule) -# timezone = try(local.stack_configs[each.key].drift_detection_timezone, var.stack_drift_detection_timezone) -# } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf deleted file mode 100644 index f9d23f1..0000000 --- a/examples/complete/outputs.tf +++ /dev/null @@ -1 +0,0 @@ -# complete.tf diff --git a/examples/complete/providers.tf b/examples/complete/providers.tf index 34f0102..c95d538 100644 --- a/examples/complete/providers.tf +++ b/examples/complete/providers.tf @@ -1,6 +1 @@ -#provider "spacelift" {} -provider "spacelift" { - api_key_endpoint = "https://masterpointio.app.spacelift.io" - api_key_id = "01JAQWRR9Q71WNQ345CF2X6DNQ" - api_key_secret = "b2a142ab75da6572f29ccac4daeb13fb39606ca430c127f98da7619af934e7c8" -} +provider "spacelift" {} diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf deleted file mode 100644 index 990ff77..0000000 --- a/examples/complete/variables.tf +++ /dev/null @@ -1,247 +0,0 @@ -# AWS -# variable "aws_role_arn" { -# type = string -# description = "ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment" -# default = null -# } - -# variable "aws_role_enabled" { -# type = bool -# description = <<-EOT -# Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role -# and put its temporary credentials in the runtime environment -# EOT -# default = true -# } - -# GitHub -# variable "github_enterprise" { -# type = object({ -# namespace = string -# id = optional(string) -# }) -# description = "The GitHub VCS settings" -# } - -# variable "repository" { -# type = string -# description = "The name of your infrastructure repo" -# } - -# variable "branch" { -# type = string -# description = "Specify which branch to use within the infrastructure repository." -# default = "main" -# } - -# variable "root_modules_path" { -# type = string -# description = "The path, relative to the root of the repository, where the root module can be found." -# default = "root-modules" -# } - -# # Spacelift Backend -# variable "terraform_workflow_tool" { -# type = string -# description = <<-EOT -# Defines the tool that will be used to execute the workflow. -# This can be one of OPEN_TOFU, TERRAFORM_FOSS or CUSTOM. Defaults to TERRAFORM_FOSS. -# EOT -# default = "OPEN_TOFU" - -# validation { -# condition = contains(["OPEN_TOFU", "TERRAFORM_FOSS", "CUSTOM"], var.terraform_workflow_tool) -# error_message = "Valid values for terraform_workflow_tool are (OPEN_TOFU, TERRAFORM_FOSS, CUSTOM)." -# } -# } - -# Default Stack Configuration -variable "stack_administrative" { - type = bool - description = "Flag to mark the stack as administrative" - default = false -} - -variable "stack_after_apply" { - type = list(string) - description = "List of after-apply scripts" - default = [] -} - -variable "stack_after_destroy" { - type = list(string) - description = "List of after-destroy scripts" - default = [] -} - -variable "stack_after_init" { - type = list(string) - description = "List of after-init scripts" - default = [] -} - -variable "stack_after_perform" { - type = list(string) - description = "List of after-perform scripts" - default = [] -} - -variable "stack_after_plan" { - type = list(string) - description = "List of after-plan scripts" - default = [] -} - -variable "stack_autodeploy" { - type = bool - description = "Flag to enable/disable automatic deployment of the stack" - default = true -} - -variable "stack_autoretry" { - type = bool - description = "Flag to enable/disable automatic retry of the stack" - default = false -} - -variable "stack_before_apply" { - type = list(string) - description = "List of before-apply scripts" - default = [] -} - -variable "stack_before_destroy" { - type = list(string) - description = "List of before-destroy scripts" - default = [] -} - -variable "stack_before_init" { - type = list(string) - description = "List of before-init scripts" - default = [] -} - -variable "stack_before_perform" { - type = list(string) - description = "List of before-perform scripts" - default = [] -} - -variable "stack_before_plan" { - type = list(string) - description = "List of before-plan scripts" - default = [""] -} - -variable "stack_description" { - type = string - description = "Description of the stack" - default = "Managed by spacelift-automation Terraform root module." -} - -variable "stack_destructor_enabled" { - type = bool - description = "Flag to enable/disable the destructor for the Stack." - default = false -} - -variable "stack_drift_detection_enabled" { - type = bool - description = "Flag to enable/disable Drift Detection configuration for a Stack." - default = false -} - -variable "stack_drift_detection_ignore_state" { - type = bool - description = <<-EOT - Controls whether drift detection should be performed on a stack - in any final state instead of just 'Finished'. - EOT - default = false -} - -variable "stack_drift_detection_reconcile" { - type = bool - description = "Flag to enable/disable automatic reconciliation of drifts." - default = false -} - -variable "stack_drift_detection_schedule" { - type = list(string) - description = "The schedule for drift detection." - default = ["0 4 * * *"] -} - -variable "stack_drift_detection_timezone" { - type = string - description = "The timezone for drift detection." - default = "UTC" -} - -variable "stack_enable_local_preview" { - type = bool - description = "Indicates whether local preview runs can be triggered on this Stack." - default = false - -} -variable "stack_manage_state" { - type = bool - description = "Determines if Spacelift should manage state for this stack." - default = false -} - -variable "stack_protect_from_deletion" { - type = bool - description = "Protect this stack from accidental deletion. If set, attempts to delete this stack will fail." - default = false -} - -variable "stack_space_id" { - type = string - description = "Place the stack in the specified space_id." - default = "root" -} - -variable "stack_terraform_smart_sanitization" { - type = bool - description = <<-EOT - Indicates whether runs on this will use terraform's sensitive value system to sanitize - the outputs of Terraform state and plans in spacelift instead of sanitizing all fields. - EOT - default = false -} - -variable "stack_terraform_version" { - type = string - description = "Terraform version to use." - default = "1.7.2" -} - -variable "stack_worker_pool_id" { - type = string - description = <<-EOT - ID of the worker pool to use. - NOTE: worker_pool_id is required when using a self-hosted instance of Spacelift. - EOT - default = null -} - -variable "root_modules" { - description = "Map of modules, each containing one or more stacks configured for Spacelift." - type = map(object({ - # These are the common configurations for all stacks created for the root module workspaces - common_stack_configs = optional(object({ - administrative = optional(bool) - autodeploy = optional(bool) - depends_on_stack_ids = optional(list(string)) - description = optional(string) - space_id = optional(string) - worker_pool_id = optional(string) - })) - # These are the configurations for each stack created for the root module workspaces. - # The overrides will take precedence over the common configurations. - stacks = optional(any) - })) - default = {} -} diff --git a/modules/spacelift-automation/.terraform.lock.hcl b/modules/spacelift-automation/.terraform.lock.hcl deleted file mode 100644 index d44028a..0000000 --- a/modules/spacelift-automation/.terraform.lock.hcl +++ /dev/null @@ -1,77 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/cloudposse/template" { - version = "2.2.0" - constraints = ">= 2.2.0" - hashes = [ - "h1:8mjJmdZNNusT4C5jUEkmGG7cm5ldxF0vvEi1iHaZvXw=", - "zh:0b547b32c16512eecc057a01287c849a397542a01c654b47fd93697a49769987", - "zh:18775d308a3df88b5adeac74ac3213d99659416b34703fbd0c0ffe9097677a2c", - "zh:1c41597c61ab67c590b754a353ed314a0ec10aeaf1ddd9ec498b953a8b735e4b", - "zh:467a65210c870a2937d2562f2316c996163ed4fa9e872e53a0cb47a7683b2e8c", - "zh:5f1c2587618fd22db24e6a35c29424464f7c0412f1b77374908809ca4e6ee3f2", - "zh:7f69a0875424cbdd5d25549d32586e321b6ea34cf65d02899f98b97913b1e4a2", - "zh:92495e3ba9d160ff1f548410b9a87c2b0d09bb58ad36e7f5f86f5c084e794e69", - "zh:aa21ddc6f3fb461acabf961c2133ceee48252542219840109df45aa91090bf7c", - "zh:b4007ea8e4fc0791a834587fc3e1cc305e17ceac837e5ae3e4649ef6b90ffe44", - "zh:b515db9619d9625a855f72147fd411e8c836b166681cc3a02cf5d1b5c6376736", - "zh:e32098a7f07e12ee47e6401956f43aaacc87c7b2ef103d6e2b9bce0f9efe5d4b", - "zh:e33f3f647e33585e2c8e6820d07cf3f022ffb3d9cd1bdd640de60e4428ffc85c", - "zh:ecdad290984ab75ce8b05f6f1cc876de82fd48b8609c5f857c7994b9f06c44f8", - ] -} - -provider "registry.opentofu.org/cloudposse/utils" { - version = "1.26.0" - constraints = ">= 0.3.0" - hashes = [ - "h1:vUsnkw4ggrBD2CI6fLoEn5329mWNCYEoca+gQJ7hjGI=", - "zh:0f1c2d5d2f0b32808292422bf8e33a5c2e3bf73602493df04a3c6c2de95c99a5", - "zh:206d84faff8a71ca81c68e25769d0f742788ca748097135adf347613ba4d9678", - "zh:2bd76df80e73e3c0581d3c05fd13a31243f286f856fe99b1e8fb06b9ce61be46", - "zh:2be74f262ad50e1b6ff0b89ab6e0f07041f82e41d3370e007f1bb07f65139760", - "zh:2f5b109bb6680c73660a30ea39426aa8ff7757c6f980020c6d200f2e92a65fb7", - "zh:32665992931644a5cdacea1af395684e607f921bb07086f462eb73124b5a0b16", - "zh:3817b6fb65621f46de467be0eb54e4e1b5d826f19d5343d0004e9e65b61884c2", - "zh:5d3fdd054cf47874280b98ec5899d4b7bcb1c58dc544a9638eac455844132214", - "zh:881d9487bf1b8b5d2a7cf76fdabe4c2a6ad43b71830c275d0b43184ad694d88b", - "zh:b6b5950e56001738ea4ab52e3a83f764ec8a31588a8278959c64c8ed69528e6b", - "zh:d8bda2f61081798afee1eb8c85aa6110a14d7d2304137c2794fe07628bb7ea1a", - "zh:d901dab8093175a398c9007e22b343e56f9eb34562cb132953d6bc03c0a887a6", - "zh:ebb90cadc9bbc36f5a202998ad413addcbd8fb57ed60d1aef798436b556eac3a", - "zh:ed6739305465c0c4a96e57b03eae3e421e9754d4c7fb42f1cbf35de82e8f29c7", - ] -} - -provider "registry.opentofu.org/hashicorp/http" { - version = "3.4.5" - constraints = ">= 2.0.0" - hashes = [ - "h1:P3NFKZbtHuQ6mmoDVpg4WYlDJ+yK4cchzkjTPzBWG3Y=", - "zh:055a4431d33bb89b9848193152433eaead7cc2e6746d3436a5922419de2112bf", - "zh:0bfabafea9f5e36802fcfc5a800831ec1767d896af889abc610014d02b09bdc2", - "zh:300b4983fe1b43bd0a7dac1f94b30b3814f11c824224dd83fb45a521c02cea60", - "zh:68f6958314ca5dc0868be70e37ec123b99b8828aa49f27fd2fdd13df05d31ab1", - "zh:c29f098a597250adc2a7d9f99acbce3c9e07d37f1c5cfded5df4309738cf613c", - "zh:c33607397f9c9302c0cd797c8b7484c9c6cfa09c3489d4b55af17df20b204368", - "zh:d519ca364a224110428b390ee06e963a3ec4dfdd1ac816c9f32e647567957cf5", - "zh:e4a9c7c0ac31a0192362ef43449390cdf00d2cf6f13061ef730b177eaf00ac45", - "zh:f25223c062f274d8f89bb96017e73586030a205bc91cdad266a9954d0def2a23", - "zh:fd4dc824ebae2f3a66318df364bec83b88e9a52e7f66b00dafa29a796d9a94ab", - ] -} - -provider "registry.opentofu.org/spacelift-io/spacelift" { - version = "1.16.1" - constraints = "~> 1.14" - hashes = [ - "h1:J3/WR9swDZOAge/mfWtbqInFQeaAtpV3/Brxrk2LbG4=", - "zh:284045c910a2c22b2c68b3a805fd01b200f1434fd4d285e9bd47bf655ddcfd18", - "zh:8a17eea1a1c438fcd9f236b1165c292791e0dc079364c0c628d25ba3f954dcd5", - "zh:8da694a38a174e69b074c6104c05ea3a691228adbbdca1c5771b56fb436e3c20", - "zh:8f2d3ba55a05ff3723c2d0c158f68bd3dfb8038169ebbef113bb88b729494785", - "zh:9a49905594efb11ec9039057bb5101d8e7ad6e2c7cf0dfb0ea09873dc3433e76", - "zh:a976fe76e94fb1521a8bc00b3a885d0ae666d552f6aed44563847ea51a027f78", - ] -} diff --git a/modules/spacelift-automation/README.md b/modules/spacelift-automation/README.md index c79084a..3db2fa6 100644 --- a/modules/spacelift-automation/README.md +++ b/modules/spacelift-automation/README.md @@ -1,83 +1,186 @@ # `spacelift-automation` -This Terraform root module provides infrastructure automation for Masterpoint's internal projects in [Spacelift](https://docs.spacelift.io/). +[![Release](https://img.shields.io/github/release/masterpointio/terraform-spacelift-automation.svg)](https://github.com/masterpointio/terraform-spacelift-automation/releases/latest) + +This Terraform root module provides infrastructure automation for projects in [Spacelift](https://docs.spacelift.io/). ## Overview The `spacelift-automation` root module is designed to streamline the deployment and management of all Spacelift infrastructure, including itself. -At the moment we have one common [spacelift-automation-mp-main](https://masterpointio.app.spacelift.io/stack/spacelift-automation-mp-main) stack that manages all the clients and internal stacks. - -It automates the creation of "child" stacks and all the required accompanying Spacelift resources. For each root module configured in the `root-modules/spacelift-automation/tfvars/mp-automation.tfvars` it creates: +It automates the creation of "child" stacks and all the required accompanying Spacelift resources. For each configured root module it creates: 1. Spacelift [Stack](https://docs.spacelift.io/concepts/stack/) You can think about a stack as a combination of source code, state file and configuration in the form of environment variables and mounted files. -1. Spacelift [Stack Destructor](https://docs.spacelift.io/concepts/stack/stack-dependencies.html#ordered-stack-creation-and-deletion) +2. Spacelift [Stack Destructor](https://docs.spacelift.io/concepts/stack/stack-dependencies.html#ordered-stack-creation-and-deletion) Required to destroy the resources of a Stack before deleting it. Destroying this resource will delete the resources in the stack. If this resource needs to be deleted and the resources in the stacks are to be preserved, ensure that the deactivated attribute is set to true. -1. Spacelift [AWS Role](https://docs.spacelift.io/integrations/cloud-providers/aws#lets-explain) - Represents cross-account IAM role delegation between the Spacelift worker and an individual stack or module. -1. Spacelift [Mounted File](https://docs.spacelift.io/concepts/configuration/environment.html#mounted-files) - This feature allows to manage the OpenTofu variables by mounting corresponding tfvars files into a working directory during the run and inject variable definitions into the OpenTofu execution environment. +3. Spacelift [AWS Integration](https://docs.spacelift.io/integrations/cloud-providers/aws#lets-explain) + It allows either Spacelift runs or tasks to automatically assume an IAM role in your AWS account. +4. Spacelift [Mounted File](https://docs.spacelift.io/concepts/configuration/environment.html#mounted-files) + This feature allows to manage the Terraform variables by mounting corresponding tfvars files into a working directory during the run and inject variable definitions into the Terraform execution environment. -These files are automatically mounted and available during the OpenTofu plan and apply stages. - -⚠️ `spacelift-automation-mp-main` manages all the mounted files and, hence, all the tfvars attached to the stacks. It's crucial to understand that a commit must kick the `spacelift-automation-mp-main` stack first to update all the necessary mounted files, and the dependent stacks run will be started. This flow is managed with Push and Trigger Spacelift Policies. Our custom Policies are stored in [config/spacelift-policies](../../config/spacelift-policies/) directory. +These files are automatically mounted and available during the Terraform plan and apply stages. ## Usage -Due to the project specifics, Spacelift Automation logic heavily relies on the Git repository structure. +Spacelift Automation logic is pretty opiniated and heavily relies on the Git repository structure. The root module `spacelift-automation` is configured to track all the files in the provided root module directory and create the stack based on the provided configuration (if any). +Structure requirements are: + +- Stack configs are placed in `/stacks` directory. +- Stack config name must be equal to OpenTofu/Terraform workspace. +- Common configs are placed in `/stacks/common.yaml` file. This is useful when you know that some values should be share across all the stacks created for a root module, e.g. all stacks that manage Spacelift Policy must be Administrative. You can override file name using Terraform variables. + Let's check the example. Input repo structure: -``` +```sh ├── root-modules │ ├── spacelift-aws-role +│ │ ├── stacks +│ │ │ └── dev.yaml +│ │ │ └── stage.yaml +│ │ │ └── common.yaml │ │ ├── tfvars -│ │ │ └── automation.tfvars │ │ │ └── dev.tfvars +│ │ │ └── stage.tfvars │ │ ├── variables.tf │ │ └── versions.tf -│ └── spacelift-spaces -│ └── tfvars -│ └── mp-automation.tfvars ... ``` Root module inputs: ```hcl -aws_role_arn = "arn:aws:iam::755965222190:role/Spacelift" +aws_integration_id = "ZDPP8SKNVG0G27T4" # GitHub configuration github_enterprise = { namespace = "masterpointio" } -repository = "spacelift-certification" +repository = "terraform-spacelift-automation" # Stacks configurations -root_modules = { - spacelift-aws-role = { - common_stack_configs = { - autodeploy = true - } - stacks = { - automation = { - description = "The AWS IAM role to assume and put its temporary credentials in `masterpoint-automation` in the runtime environment." - space_id = "mp-automation-01J2BSSM6TW46GJ6EJNZW1WP2B" - } - } - } - spacelift-spaces = {} -} +root_modules_path = "root-modules" +enabled_root_modules = ["spacelift-aws-role"] ``` The configuration above creates the following stacks: -- `spacelift-aws-role-automation` with the overridden `autodeploy`, `space_id` and `description` values -- `spacelift-aws-role-dev` with the overriden `autodeploy` valuee -- `spacelift-spaces-mp-automation` with the default stack values - -Corresponding Terraform variables are mounted to each stack. For example, the content of the file `root-modules/spacelift-policies/tfvars/push-default.tfvars` will be mounted to the stack `spacelift-policies-push-default`, allowing the Terraform inputs to be provided to the configuration. +- `spacelift-aws-role-dev` +- `spacelift-aws-role-stage` + +* Common configuration set in `root-modules/spacelift-aws-role/stacks/common.yaml` are applied to both Stacks. +* However, if there is an override in a Stack config, it takes precedence over common configs. +* Corresponding Terraform variables are generated by [Initialization Hook](https://docs.spacelift.io/concepts/run#initializing) and placed in the root of each Stack's working directory during each run or task. For example, the content of the file `root-modules/spacelift-aws-role/tfvars/dev.tfvars` will be copied to the Stack `spacelift-aws-role-dev` as file `spacelift.auto.tfvars` allowing the OpenTofu/Terraform inputs to be automatically loaded. + +## Bootstrap + +Spacelift Automation manage itself as a Stack as well: + +1. Create a vanilla OpenTofu/Terraform configuration for this module. +1. Create OpenTofu/Terraform workspace that will be used for your Automation configuration. +1. Apply the vanilla OpenTofu/Terraform configuration. +1. Move the Automation configs to the `/spacelift-automation/stacks` directory and push the changes to the tracked repo and branch. +1. From this moment Spacelift Automation is tracking the changes for its Stack configs. + +NOTE to Masterpoint team: We might want to create a small wrapper to automatize this using Taskit. + + + +## Requirements + +| Name | Version | +| ------------------------------------------------------------------------ | ------- | +| [terraform](#requirement_terraform) | >= 1.6 | +| [spacelift](#requirement_spacelift) | >= 1.14 | + +## Providers + +| Name | Version | +| ------------------------------------------------------------------ | ------- | +| [spacelift](#provider_spacelift) | >= 1.14 | + +## Modules + +| Name | Source | Version | +| ----------------------------------------------- | ----------------------------------------- | ------- | +| [deep](#module_deep) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | +| [this](#module_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | +| [spacelift_aws_integration_attachment.this](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/aws_integration_attachment) | resource | +| [spacelift_drift_detection.this](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/drift_detection) | resource | +| [spacelift_stack.this](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/stack) | resource | +| [spacelift_stack_destructor.this](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/stack_destructor) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | +| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [administrative](#input_administrative) | Flag to mark the stack as administrative | `bool` | `false` | no | +| [after_apply](#input_after_apply) | List of after-apply scripts | `list(string)` | `[]` | no | +| [after_destroy](#input_after_destroy) | List of after-destroy scripts | `list(string)` | `[]` | no | +| [after_init](#input_after_init) | List of after-init scripts | `list(string)` | `[]` | no | +| [after_perform](#input_after_perform) | List of after-perform scripts | `list(string)` | `[]` | no | +| [after_plan](#input_after_plan) | List of after-plan scripts | `list(string)` | `[]` | no | +| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [autodeploy](#input_autodeploy) | Flag to enable/disable automatic deployment of the stack | `bool` | `true` | no | +| [autoretry](#input_autoretry) | Flag to enable/disable automatic retry of the stack | `bool` | `false` | no | +| [aws_integration_attachment_read](#input_aws_integration_attachment_read) | Indicates whether this attachment is used for read operations. | `bool` | `true` | no | +| [aws_integration_attachment_write](#input_aws_integration_attachment_write) | Indicates whether this attachment is used for write operations. | `bool` | `true` | no | +| [aws_integration_id](#input_aws_integration_id) | ID of the AWS integration to attach. | `string` | n/a | yes | +| [before_apply](#input_before_apply) | List of before-apply scripts | `list(string)` | `[]` | no | +| [before_destroy](#input_before_destroy) | List of before-destroy scripts | `list(string)` | `[]` | no | +| [before_init](#input_before_init) | List of before-init scripts | `list(string)` | `[]` | no | +| [before_perform](#input_before_perform) | List of before-perform scripts | `list(string)` | `[]` | no | +| [before_plan](#input_before_plan) | List of before-plan scripts | `list(string)` | `[]` | no | +| [branch](#input_branch) | Specify which branch to use within the infrastructure repository. | `string` | `"main"` | no | +| [common_config_file](#input_common_config_file) | Name of the common configuration file for the stack across a root module. | `string` | `"common.yaml"` | no | +| [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input_description) | Description of the stack | `string` | `"Managed by spacelift-automation Terraform root module."` | no | +| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [destructor_enabled](#input_destructor_enabled) | Flag to enable/disable the destructor for the Stack. | `bool` | `false` | no | +| [drift_detection_enabled](#input_drift_detection_enabled) | Flag to enable/disable Drift Detection configuration for a Stack. | `bool` | `false` | no | +| [drift_detection_ignore_state](#input_drift_detection_ignore_state) | Controls whether drift detection should be performed on a stack
in any final state instead of just 'Finished'. | `bool` | `false` | no | +| [drift_detection_reconcile](#input_drift_detection_reconcile) | Flag to enable/disable automatic reconciliation of drifts. | `bool` | `false` | no | +| [drift_detection_schedule](#input_drift_detection_schedule) | The schedule for drift detection. | `list(string)` |
[
"0 4 * * *"
]
| no | +| [drift_detection_timezone](#input_drift_detection_timezone) | The timezone for drift detection. | `string` | `"UTC"` | no | +| [enable_local_preview](#input_enable_local_preview) | Indicates whether local preview runs can be triggered on this Stack. | `bool` | `false` | no | +| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [enabled_root_modules](#input_enabled_root_modules) | List of root modules where to look for stack config files. | `list(string)` | `[]` | no | +| [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github_enterprise](#input_github_enterprise) | The GitHub VCS settings |
object({
namespace = string
id = optional(string)
})
| n/a | yes | +| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [manage_state](#input_manage_state) | Determines if Spacelift should manage state for this stack. | `bool` | `false` | no | +| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [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 | +| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [repository](#input_repository) | The name of your infrastructure repo | `string` | n/a | yes | +| [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 | +| [space_id](#input_space_id) | Place the stack in the specified space_id. | `string` | `"root"` | no | +| [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [terraform_smart_sanitization](#input_terraform_smart_sanitization) | Indicates whether runs on this will use terraform's sensitive value system to sanitize
the outputs of Terraform state and plans in spacelift instead of sanitizing all fields. | `bool` | `false` | no | +| [terraform_version](#input_terraform_version) | Terraform version to use. | `string` | `"1.7.2"` | no | +| [terraform_workflow_tool](#input_terraform_workflow_tool) | Defines the tool that will be used to execute the workflow.
This can be one of OPEN_TOFU, TERRAFORM_FOSS or CUSTOM. Defaults to TERRAFORM_FOSS. | `string` | `"OPEN_TOFU"` | no | +| [worker_pool_id](#input_worker_pool_id) | ID of the worker pool to use.
NOTE: worker_pool_id is required when using a self-hosted instance of Spacelift. | `string` | `null` | no | + +## Outputs + +No outputs. + + diff --git a/modules/spacelift-automation/context.tf b/modules/spacelift-automation/context.tf index 8b703cd..5e0ef88 100644 --- a/modules/spacelift-automation/context.tf +++ b/modules/spacelift-automation/context.tf @@ -8,6 +8,8 @@ # Cloud Posse's standard configuration inputs suitable for passing # to Cloud Posse modules. # +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# # Modules should access the whole context as `module.this.context` # to get the input variables with nulls for defaults, # for example `context = module.this.context`, @@ -20,10 +22,11 @@ module "this" { source = "cloudposse/label/null" - version = "0.24.1" + version = "0.25.0" # requires Terraform >= 0.13.0 enabled = var.enabled namespace = var.namespace + tenant = var.tenant environment = var.environment stage = var.stage name = var.name @@ -34,6 +37,10 @@ module "this" { label_order = var.label_order regex_replace_chars = var.regex_replace_chars id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags context = var.context } @@ -41,23 +48,11 @@ module "this" { # Copy contents of cloudposse/terraform-null-label/variables.tf here variable "context" { - type = object({ - enabled = bool - namespace = string - environment = string - stage = string - name = string - delimiter = string - attributes = list(string) - tags = map(string) - additional_tag_map = map(string) - regex_replace_chars = string - label_order = list(string) - id_length_limit = number - }) + type = any default = { enabled = true namespace = null + tenant = null environment = null stage = null name = null @@ -68,6 +63,17 @@ variable "context" { regex_replace_chars = null label_order = [] id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] } description = <<-EOT Single object for setting entire context at once. @@ -76,6 +82,16 @@ variable "context" { Individual variable settings (non-null) override settings in context object, except for attributes, tags, and additional_tag_map, which are merged. EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } } variable "enabled" { @@ -87,32 +103,42 @@ variable "enabled" { variable "namespace" { type = string default = null - description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" } variable "environment" { type = string default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" } variable "stage" { type = string default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" } variable "name" { type = string default = null - description = "Solution name, e.g. 'app' or 'jenkins'" + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT } variable "delimiter" { type = string default = null description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. + Delimiter to be used between ID elements. Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. EOT } @@ -120,36 +146,64 @@ variable "delimiter" { variable "attributes" { type = list(string) default = [] - description = "Additional attributes (e.g. `1`)" + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT } variable "tags" { type = map(string) default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT } variable "additional_tag_map" { type = map(string) default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT } variable "label_order" { type = list(string) default = null description = <<-EOT - The naming order of the id output and Name tag. + The order in which the labels (ID elements) appear in the `id`. Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, but at least one must be present. - EOT + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT } variable "regex_replace_chars" { type = string default = null description = <<-EOT - Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. EOT } @@ -158,11 +212,68 @@ variable "id_length_limit" { type = number default = null description = <<-EOT - Limit `id` to this many characters. + Limit `id` to this many characters (minimum 6). Set to `0` for unlimited length. - Set to `null` for default, which is `0`. + Set to `null` for keep the existing setting, which defaults to `0`. Does not affect `id_full`. EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT } #### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spacelift-automation/default.auto.tfvars b/modules/spacelift-automation/default.auto.tfvars deleted file mode 100644 index 8778d81..0000000 --- a/modules/spacelift-automation/default.auto.tfvars +++ /dev/null @@ -1,14 +0,0 @@ -enabled = true - -# For testing: -#aws_role_arn = "arn:aws:iam::755965222190:role/Spacelift" # Need to replace with AWS integration - -github_enterprise = { - namespace = "masterpointio" -} -repository = "terraform-spacelift-automation" -branch = "feature/initial-version" - -root_modules_path = "../../examples/complete/components" -enabled_root_modules = ["spacelift-policies", "random-pet"] -aws_integration_id = "01J30JBKQTCD72ATZCRWHYST3C" diff --git a/modules/spacelift-automation/main.tf b/modules/spacelift-automation/main.tf index 9c1af4e..a129483 100644 --- a/modules/spacelift-automation/main.tf +++ b/modules/spacelift-automation/main.tf @@ -5,31 +5,19 @@ # This module can also manage the automation stack intself, but it should be bootsrapped manually. # # It handles the following: -# 1. Workspaces (see ## Workspaces) -# Reads workspace names for each root module set in the root_modules variable in the spacelift-automation -# Workspace names are extracted from the files in each root module `tfvars` directory, and are equal to the file names. -# Stacks names are build from the root module name and workspace name. # -# 2. Stack Configurations from Git (see ## Stack Configurations from Git) +# 1. Stack Configurations from Git (see ## Stack Configurations from Git) # Reads the Spacelift stack configurations strictly based on the root modules structure in Git and file names. # These are the configurations required to be set for a stack, e.g. project_root, terraform_workspace, root_module. # -# 3. Stack Configurations from Terraform variables (see ## Stack Configurations from Terraform variables) -# Reads the Spacelift stack configurations explicitly specified in the spacelift-automation tfvars file. -# These configurations are intended to override the default values. -# -# 4. Common Stack configrations -# Some configurations are euqal across the whole root module, and can be set it on a root module level: -# * Space IDs: in the majority if cases all the workspaces in a root module belong to the same Spacelift space, so +# 2. Common Stack configurations +# Some configurations are equal across the whole root module, and can be set it on a root module level: +# * Space IDs: in the majority of cases all the workspaces in a root module belong to the same Spacelift space, so # we allow setting a "global" space_id for all stacks on a root module level. # * Autodeploy: if all the stacks in a root module should be autodeployed. # * Administrative: if all the stacks in a root module are administrative, e.g stacks that manage Spacelift resources. # -# 5. Dependencies (see ## Dependencies) -# Builds stack dependencies based on the root modules configuration. -# Our main case is to support the dependency from each child from its parent stack, which is spacelift-automation-. -# -# 6. Labels (see ## Labels) +# 3. Labels (see ## Labels) # Generates labels for the stacks based on administrative, dependency, and folder information. # # Syntax note: @@ -37,231 +25,48 @@ # that are not directly used in the resource creation. locals { - enabled = module.this.enabled - aws_role_enabled = local.enabled && var.aws_role_enabled + enabled = module.this.enabled - ## Workspaces - # Extracts the list of workspace names from tfvars files for each given root module. - # Root module name is used as a key of the map. - # For root_modules = { - # "client-infra" = {}, - # "spacelift-automation" = { - # stacks = { - # mp-automation = { - # administrative = true - # description = "Administrative Spacelift Stack for managing Masterpoint's Infrastructure." - # } - # } - # } - # } - # If the tfvars directory for `client-infra` contains files "client1.tfvars" and "client2.tfvars", - # and for `spacelift-automation` it contains "mp-automation.tfvars": - # The resulting workspaces will be: - # { - # "client-infra" = [ - # "client1", - # "client2", - # ] - # "spacelift-automation" = [ - # "mp-automation", - # ] - # } - - _workspaces = { - for key in keys(var.root_modules) : key => [ - for file in fileset(format("../../%s/%s/tfvars/", var.root_modules_path, key), "*.tfvars") : - trimsuffix(file, ".tfvars") - ] - } - - ## Stack Configurations from Git - # Creates a map of the Git based stack configurations for each root module. - # Example for workspaces `client1` and `client2` in `client-infra` root module: - # { - # "client-infra" = { - # "client-infra-client1" = { - # "terraform_workspace" = "client1" - # "project_root" = "root-modules/client-infra" - # "root_module" = "client-infra" - # } - # "client-infra-client2" = { - # "terraform_workspace" = "client2" - # "project_root" = "root-modules/client-infra" - # "root_module" = "client-infra" - # } - # } - # } - - _git_stack_configs = { - for module, workspaces in local._workspaces : module => { - for workspace in workspaces : "${module}-${workspace}" => - { - terraform_workspace = workspace, - project_root = format("%s/%s", var.root_modules_path, module) - root_module = module - } + # Read and decode Stack YAML files from the root directory + root_module_yaml_decoded = { + for module in var.enabled_root_modules : module => { + for yaml_file in fileset("${path.root}/${var.root_modules_path}/${module}/stacks", "*.yaml") : + yaml_file => yamldecode(file("${path.root}/${var.root_modules_path}/${module}/stacks/${yaml_file}")) } } - # Flatten the stack configurations based into a single map. - # Example for the `client-infra` root module: - # { - # "client-infra-client1" = { - # "terraform_workspace" = "client1" - # "project_root" = "root-modules/client-infra" - # "root_module" = "client-infra" - # } - # "client-infra-client2" = { - # "terraform_workspace" = "client2" - # "project_root" = "root-modules/client-infra" - # "root_module" = "client-infra" - # } - # } - - _flat_git_stack_configs = merge([ - for module, stacks in local._git_stack_configs : { - for stack_name, stack in stacks : stack_name => stack - } - ]...) - - ## Stack Configurations from Terraform variables - # Creates a map of the stack configurations that should be created for each root module specified in - # the spacelift-automation tfvars file. - # Example for workspaces `trigger-automated-retries` and `trigger-dependencies` in `spacelift-policies` root module: - # { - # "spacelift-policies" = { - # "spacelift-policies-trigger-automated-retries" = { - # "administrative" = true - # } - # "spacelift-policies-trigger-dependencies" = { - # "administrative" = true - # "description" = "Policy to trigger other stacks." - # } - # } - - _tfvars_stack_configs = { - for module_name, configs in var.root_modules : module_name => { - for workspace_name, workspace_configs in coalesce(configs.stacks, {}) : "${module_name}-${workspace_name}" => workspace_configs - } + # Retrieve common Stack configurations for each root module + common_configs = { + for module, file in local.root_module_yaml_decoded : module => lookup(file, var.common_config_file, {}) } - # Flatten the configured stacks into a single map and merge with the common stack configurations. - # Example for the `spacelift-policies` root module: - # { - # "spacelift-policies-trigger-automated-retries" = { - # "administrative" = true - # } - # "spacelift-policies-trigger-dependencies" = { - # "administrative" = true - # "description" = "Policy to trigger other stacks." - # } - # } - - _flat_tfvars_stack_configs = merge([ - for module, stack in local._tfvars_stack_configs : { - for stack_name, stack_config in stack : stack_name => stack_config + # Merge all Stack configurations from the root modules into a single map + root_module_stack_configs = merge([for module, files in local.root_module_yaml_decoded : { + for file, content in files : "${module}-${trimsuffix(file, ".yaml")}" => + merge( + { + "project_root" = replace(format("%s/%s", var.root_modules_path, module), "../", "") + "root_module" = module, + "terraform_workspace" = trimsuffix(file, ".yaml"), + }, + content + ) if file != var.common_config_file } ]...) - _common_stack_configs = { - for module, stacks in local._git_stack_configs : module => { - for stack_name, stack in stacks : stack_name => try(var.root_modules[module].common_stack_configs, {}) - } + # Get the configs for each stack, merged with the common configurations + configs = { + for key, value in module.deep : key => value.merged } - _flat_common_stack_configs = merge([ - for module, stack in local._common_stack_configs : { - for stack_name, stack_config in stack : stack_name => stack_config - } - ]...) - - # Iterate over stack configs in git and merge all stack configurations from tfvars into a single map. - # stack_configs = { - # for k, v in local._flat_git_stack_configs : k => merge( - # v, - # try(local._flat_common_stack_configs[k], {}), - # try(local._flat_tfvars_stack_configs[k], {}), - # ) - # } + # Get the Stacks configs, this is just to improve code readability + stack_configs = { + for key, value in local.configs : key => value.stack_settings + } # Get the list of all stack names - #stacks = toset(keys(local.stack_configs)) stacks = toset(keys(local.stack_configs)) - ## Dependencies - - # Get the dependencies from the root modules configuration. - # Expected to be set on a per-module. Might want to revisit this in the future. - # Child stacks always depend on the spacelift-automation stack. - # Example for the `client-infra` root module: - # { - # "client-infra" = { - # "depends_on_stack_ids" = tolist([ - # "spacelift-automation-mp-main", - # "spacelift-webhooks-slack-notifications", - # ]) - # } - # } - - _module_depends_on_stack_ids = { - for module_name, module in var.root_modules : module_name => { - depends_on_stack_ids = [format("%s-%s", "spacelift-automation", terraform.workspace)] - } - } - # Creates a map of the dependencies list based for each stack. - # Example for the `client-infra` root module: - # { - # "client-infra-client1" = { - # depends_on_stack_ids = ["spacelift-automation-mp-main", "spacelift-webhooks-slack-notifications"] - # } - # "client-infra-client2" = { - # depends_on_stack_ids = ["spacelift-automation-mp-main", "spacelift-webhooks-slack-notifications"] - # } - # } - - _depends_on_stack_ids = merge([ - for module, stacks in local._git_stack_configs : { - for stack_name, stack in stacks : stack_name => { - depends_on_stack_ids = concat( - values(lookup(local._module_depends_on_stack_ids, module, [])), - try(local.stack_configs[stacks.stack_name].depends_on_stack_ids, []) - ) - } - } - ]...) - - # Break dependencies into a flat list. - # Example for the `client-infra` root module: - # [ - # { - # stack_id = "client-infra-client1" - # depends_on_stack_id = "spacelift-automation-mp-main" - # }, - # { - # stack_id = "client-infra-client1" - # depends_on_stack_id = "spacelift-webhooks-slack-notifications" - # }, - # { - # stack_id = "client-infra-client2" - # depends_on_stack_id = "spacelift-automation-mp-main" - # }, - # { - # stack_id = "client-infra-client2" - # depends_on_stack_id = "spacelift-webhooks-slack-notifications" - # } - # ] - - _dependency_list = flatten([ - for stack_name, config in local._depends_on_stack_ids : [ - for depends_on in config.depends_on_stack_ids : [ - for id in depends_on : { - stack_id = stack_name - depends_on_stack_id = id - } - ] - ] - ]) - ## Labels # Сreates a map of administrative labels for each stack that has the administrative property set to true. # Example: @@ -269,18 +74,13 @@ locals { # "spacelift-automation-mp-main" = [ # "administrative", # ] - # "spacelift-policies-notify-stack-creation" = [ - # "administrative", - # ] # "spacelift-policies-notify-tf-completed" = [ # "administrative", # ] # } _administrative_labels = { - # Normalizing the value here is required due to TF not enforcing - # strict type-checking for `stacks = optional(any)` in var.root_modules - for stack_name, stack in local.stack_configs : stack_name => ["administrative"] if tobool(try(stack.administrative, false)) == true + for stack, configs in local.stack_configs : stack => ["administrative"] if tobool(try(configs.administrative, false)) == true } # Creates a map of `depends-on` labels for each stack based on the root module level dependency configuration. @@ -295,9 +95,8 @@ locals { # } _dependency_labels = { - for dependency in local._dependency_list : - dependency.stack_id => [ - "depends-on:${dependency.depends_on_stack_id}" + for stack in local.stacks : stack => [ + "depends-on:spacelift-automation-${terraform.workspace}" ] } @@ -305,9 +104,6 @@ locals { # https://docs.spacelift.io/concepts/stack/organizing-stacks#label-based-folders # Example: # { - # "spacelift-spaces-clients" = [ - # "folder:spacelift-spaces/clients", - # ] # "spacelift-spaces-prod" = [ # "folder:spacelift-spaces/prod", # ] @@ -316,20 +112,18 @@ locals { # ] # } - _folder_labels = merge([ - for module, stacks in local._git_stack_configs : { - for stack_name, stack in stacks : stack_name => [ - "folder:${stack.root_module}/${stack.terraform_workspace}" - ] - } - ]...) + _folder_labels = { + for stack in local.stacks : stack => [ + "folder:${local.configs[stack].root_module}/${local.configs[stack].terraform_workspace}" + ] + } - # Merge all labels into a single map for each stack. + # Merge all the labels into a single map for each stack. # Example: # { # "spacelift-spaces-clients" = [ # "folder:spacelift-spaces/clients", - # "depends-on:spacelift-automation-mp-main", + # "administrative", # ] # } @@ -352,10 +146,19 @@ locals { # It copies the tfvars file from the stack's workspace to the root module's directory # and renames it to `spacelift.auto.tfvars` to automatically load variable definitions for each run/task. ["cp tfvars/${local.configs[stack].terraform_workspace}.tfvars spacelift.auto.tfvars"], - )) if local.configs[stack].tfvars.enabled + )) if try(local.configs[stack].tfvars.enabled, false) } } +# Perform deep merge for common configurations and stack configurations +module "deep" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.2" + for_each = local.root_module_stack_configs + # Stack configuration will take precedence and overwrite the conflicting value from the common configuration (if any) + maps = [local.common_configs[each.value.root_module], each.value] +} + resource "spacelift_stack" "this" { for_each = local.enabled ? local.stacks : toset([]) @@ -415,15 +218,8 @@ resource "spacelift_stack_destructor" "this" { ] } -# resource "spacelift_aws_role" "this" { -# for_each = local.aws_role_enabled ? local.stacks : toset([]) - -# stack_id = spacelift_stack.this[each.key].id -# role_arn = var.aws_role_arn -# } - resource "spacelift_aws_integration_attachment" "this" { - for_each = local.aws_role_enabled ? local.stacks : toset([]) + for_each = local.enabled ? local.stacks : toset([]) integration_id = try(local.stack_configs[each.key].aws_integration_id, var.aws_integration_id) stack_id = spacelift_stack.this[each.key].id read = var.aws_integration_attachment_read @@ -432,7 +228,7 @@ resource "spacelift_aws_integration_attachment" "this" { resource "spacelift_drift_detection" "this" { for_each = local.enabled ? { - for key, value in local.stacks : key => value + for key, value in local.stack_configs : key => value if try(local.stack_configs[key].drift_detection_enabled, var.drift_detection_enabled) } : {} diff --git a/modules/spacelift-automation/providers.tf b/modules/spacelift-automation/providers.tf index b47c565..2809331 100644 --- a/modules/spacelift-automation/providers.tf +++ b/modules/spacelift-automation/providers.tf @@ -1,5 +1,10 @@ -provider "spacelift" { - api_key_endpoint = "https://masterpointio.app.spacelift.io" - api_key_id = "01JAQWRR9Q71WNQ345CF2X6DNQ" - api_key_secret = "b2a142ab75da6572f29ccac4daeb13fb39606ca430c127f98da7619af934e7c8" -} +# Earlier versions of OpenTofu used empty provider blocks ("proxy provider configurations") +# for child modules to declare their need to be passed a provider configuration by their +# callers. That approach was ambiguous and is now deprecated. +# +# If you control this module, you can migrate to the new declaration syntax by removing all +# of the empty provider "spacelift" blocks and then adding or updating an entry like the +# following to the required_providers block of module.automation: +# spacelift = { +# source = "spacelift-io/spacelift" +# } diff --git a/modules/spacelift-automation/refactor.tf b/modules/spacelift-automation/refactor.tf deleted file mode 100644 index 9b9f5f0..0000000 --- a/modules/spacelift-automation/refactor.tf +++ /dev/null @@ -1,45 +0,0 @@ -locals { - # Read and decode stack YAML files from the root directory - root_module_yaml_decoded = { - for module in var.enabled_root_modules : module => { - for yaml_file in fileset("${path.root}/${var.root_modules_path}/${module}/stacks", "*.yaml") : - yaml_file => yamldecode(file("${path.root}/${var.root_modules_path}/${module}/stacks/${yaml_file}")) - } - } - - # Retrieve common stack configurations for each root module - common_configs = { - for module, file in local.root_module_yaml_decoded : module => lookup(file, "common.yaml", {}) - } - - # Merge all stack configurations from the root modules into a single map - #stack_configs = local.root_module_stack_configs - root_module_stack_configs = merge([for module, files in local.root_module_yaml_decoded : { - for file, content in files : "${module}-${trimsuffix(file, ".yaml")}" => - merge( - { - "project_root" = replace(format("%s/%s", var.root_modules_path, module), "../", "") - "root_module" = module, - "terraform_workspace" = trimsuffix(file, ".yaml"), - }, - content - ) if file != "common.yaml" - } - ]...) - - configs = { - for key, value in module.deep : key => value.merged - } - - stack_configs = { - for key, value in local.configs : key => try(value.stack_settings, {}) - } -} - -# Merge stack configurations with the common configurations -module "deep" { - source = "cloudposse/config/yaml//modules/deepmerge" - version = "1.0.2" - for_each = local.root_module_stack_configs - maps = [each.value, local.common_configs[each.value.root_module]] -} diff --git a/modules/spacelift-automation/variables.tf b/modules/spacelift-automation/variables.tf index 7641a4a..ad36b3a 100644 --- a/modules/spacelift-automation/variables.tf +++ b/modules/spacelift-automation/variables.tf @@ -1,19 +1,3 @@ -# AWS -variable "aws_role_arn" { - type = string - description = "ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment" - default = null -} - -variable "aws_role_enabled" { - type = bool - description = <<-EOT - Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role - and put its temporary credentials in the runtime environment - EOT - default = true -} - # GitHub variable "github_enterprise" { type = object({ @@ -61,25 +45,6 @@ variable "terraform_workflow_tool" { } } -variable "root_modules" { - description = "Map of modules, each containing one or more stacks configured for Spacelift." - type = map(object({ - # These are the common configurations for all stacks created for the root module workspaces - common_stack_configs = optional(object({ - administrative = optional(bool) - autodeploy = optional(bool) - depends_on_stack_ids = optional(list(string)) - description = optional(string) - space_id = optional(string) - worker_pool_id = optional(string) - })) - # These are the configurations for each stack created for the root module workspaces. - # The overrides will take precedence over the common configurations. - stacks = optional(any) - })) - default = {} -} - # Stack Cloud Integrations variable "aws_integration_id" { type = string @@ -98,6 +63,12 @@ variable "aws_integration_attachment_write" { default = true } +# Configuration for the Spacelift Stack +variable "common_config_file" { + type = string + description = "Name of the common configuration file for the stack across a root module." + default = "common.yaml" +} # Default Stack Configuration variable "administrative" { type = bool @@ -174,7 +145,7 @@ variable "before_perform" { variable "before_plan" { type = list(string) description = "List of before-plan scripts" - default = [""] + default = [] } variable "description" { diff --git a/modules/spacelift-automation/versions.tf b/modules/spacelift-automation/versions.tf index 5ffc0fa..5d935ac 100644 --- a/modules/spacelift-automation/versions.tf +++ b/modules/spacelift-automation/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = "~> 1.6" + required_version = ">= 1.6" required_providers { spacelift = { source = "spacelift-io/spacelift" - version = "~> 1.14" + version = ">= 1.14" } } }