From 26d43d8ec5b50706f919b9e39bc537ba672f33c1 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 27 Nov 2025 21:51:20 +0100 Subject: [PATCH] re-enable project billing association in project factory, extends to folder (#3554) --- .../0-org-setup/schemas/folder.schema.json | 6 ++++ .../2-networking/schemas/folder.schema.json | 6 ++++ .../schemas/folder.schema.json | 6 ++++ .../2-security/schemas/folder.schema.json | 6 ++++ modules/billing-account/README.md | 12 +++---- modules/billing-account/budgets.tf | 35 ++++++++++++++----- modules/billing-account/factory.tf | 4 +-- modules/billing-account/variables.tf | 6 ++-- modules/project-factory/budgets.tf | 29 ++++++++++++++- modules/project-factory/main.tf | 2 +- .../schemas/folder.schema.json | 6 ++++ .../project_factory/examples/example.yaml | 1 - 12 files changed, 98 insertions(+), 21 deletions(-) diff --git a/fast/stages/0-org-setup/schemas/folder.schema.json b/fast/stages/0-org-setup/schemas/folder.schema.json index 837e33538..907b2dff1 100644 --- a/fast/stages/0-org-setup/schemas/folder.schema.json +++ b/fast/stages/0-org-setup/schemas/folder.schema.json @@ -74,6 +74,12 @@ } } }, + "billing_budgets": { + "type": "array", + "items": { + "type": "string" + } + }, "contacts": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-networking/schemas/folder.schema.json b/fast/stages/2-networking/schemas/folder.schema.json index 837e33538..907b2dff1 100644 --- a/fast/stages/2-networking/schemas/folder.schema.json +++ b/fast/stages/2-networking/schemas/folder.schema.json @@ -74,6 +74,12 @@ } } }, + "billing_budgets": { + "type": "array", + "items": { + "type": "string" + } + }, "contacts": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-project-factory/schemas/folder.schema.json b/fast/stages/2-project-factory/schemas/folder.schema.json index 837e33538..907b2dff1 100644 --- a/fast/stages/2-project-factory/schemas/folder.schema.json +++ b/fast/stages/2-project-factory/schemas/folder.schema.json @@ -74,6 +74,12 @@ } } }, + "billing_budgets": { + "type": "array", + "items": { + "type": "string" + } + }, "contacts": { "type": "object", "additionalProperties": false, diff --git a/fast/stages/2-security/schemas/folder.schema.json b/fast/stages/2-security/schemas/folder.schema.json index 837e33538..907b2dff1 100644 --- a/fast/stages/2-security/schemas/folder.schema.json +++ b/fast/stages/2-security/schemas/folder.schema.json @@ -74,6 +74,12 @@ } } }, + "billing_budgets": { + "type": "array", + "items": { + "type": "string" + } + }, "contacts": { "type": "object", "additionalProperties": false, diff --git a/modules/billing-account/README.md b/modules/billing-account/README.md index 6b71127a8..b3950fa15 100644 --- a/modules/billing-account/README.md +++ b/modules/billing-account/README.md @@ -277,17 +277,17 @@ update_rules: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [id](variables.tf#L145) | Billing account id. | string | ✓ | | +| [id](variables.tf#L147) | Billing account id. | string | ✓ | | | [budget_notification_channels](variables.tf#L17) | Notification channels used by budget alerts. | map(object({…})) | | {} | -| [budgets](variables.tf#L47) | Billing budgets. Notification channels are either keys in corresponding variable, or external ids. | map(object({…})) | | {} | -| [context](variables.tf#L122) | Context-specific interpolations. | object({…}) | | {} | -| [factories_config](variables.tf#L136) | Path to folder containing budget alerts data files. | object({…}) | | {} | +| [budgets](variables.tf#L47) | Billing budgets. Notification channels are either keys in corresponding variable, or external ids. | map(object({…})) | | {} | +| [context](variables.tf#L122) | Context-specific interpolations. | object({…}) | | {} | +| [factories_config](variables.tf#L138) | Path to folder containing budget alerts data files. | object({…}) | | {} | | [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | -| [logging_sinks](variables.tf#L150) | Logging sinks to create for the billing account. | map(object({…})) | | {} | -| [projects](variables.tf#L183) | Projects associated with this billing account. | list(string) | | [] | +| [logging_sinks](variables.tf#L152) | Logging sinks to create for the billing account. | map(object({…})) | | {} | +| [projects](variables.tf#L185) | Projects associated with this billing account. | list(string) | | [] | ## Outputs diff --git a/modules/billing-account/budgets.tf b/modules/billing-account/budgets.tf index 0e381f00b..aaa38ae1a 100644 --- a/modules/billing-account/budgets.tf +++ b/modules/billing-account/budgets.tf @@ -48,6 +48,19 @@ resource "google_monitoring_notification_channel" "default" { } } +# resource "terraform_data" "defaults_preconditions" { +# lifecycle { +# precondition { +# condition = local.factory_budgets == null +# error_message = yamlencode(local.factory_budgets) +# } +# precondition { +# condition = local.factory_budgets == null +# error_message = yamlencode(local.ctx.project_sets) +# } +# } +# } + resource "google_billing_budget" "default" { for_each = merge(local.factory_budgets, var.budgets) billing_account = var.id @@ -83,14 +96,20 @@ resource "google_billing_budget" "default" { labels = each.value.filter.label == null ? null : { (each.value.filter.label.key) = each.value.filter.label.value } - projects = each.value.filter.projects == null ? [] : [ - for v in each.value.filter.projects : - lookup(local.ctx.project_ids, v, v) - ] - resource_ancestors = each.value.filter.resource_ancestors == null ? [] : [ - for v in each.value.filter.resource_ancestors : - lookup(local.ctx.folder_ids, v, v) - ] + projects = concat( + [ + for v in each.value.filter.projects : + lookup(local.ctx.project_ids, v, v) + ], + lookup(local.ctx.project_sets, "$project_sets:${each.key}", []) + ) + resource_ancestors = concat( + [ + for v in each.value.filter.resource_ancestors : + lookup(local.ctx.folder_ids, v, v) + ], + lookup(local.ctx.folder_sets, "$folder_sets:${each.key}", []) + ) services = each.value.filter.services subaccounts = each.value.filter.subaccounts dynamic "custom_period" { diff --git a/modules/billing-account/factory.tf b/modules/billing-account/factory.tf index 343cfdca8..2092f1605 100644 --- a/modules/billing-account/factory.tf +++ b/modules/billing-account/factory.tf @@ -45,8 +45,8 @@ locals { ) ) label = try(v.filter.label, null) - projects = try(v.filter.projects, null) - resource_ancestors = try(v.filter.resource_ancestors, null) + projects = try(v.filter.projects, []) + resource_ancestors = try(v.filter.resource_ancestors, []) services = try(v.filter.services, null) subaccounts = try(v.filter.subaccounts, null) } diff --git a/modules/billing-account/variables.tf b/modules/billing-account/variables.tf index f9d736edd..061767540 100644 --- a/modules/billing-account/variables.tf +++ b/modules/billing-account/variables.tf @@ -79,8 +79,8 @@ variable "budgets" { })) })) })) - projects = optional(list(string)) - resource_ancestors = optional(list(string)) + projects = optional(list(string), []) + resource_ancestors = optional(list(string), []) services = optional(list(string)) subaccounts = optional(list(string)) })) @@ -124,9 +124,11 @@ variable "context" { type = object({ custom_roles = optional(map(string), {}) folder_ids = optional(map(string), {}) + folder_sets = optional(map(list(string)), {}) iam_principals = optional(map(string), {}) notification_channels = optional(map(string), {}) project_ids = optional(map(string), {}) + project_sets = optional(map(list(string)), {}) storage_buckets = optional(map(string), {}) }) default = {} diff --git a/modules/project-factory/budgets.tf b/modules/project-factory/budgets.tf index 702c01870..c7466a2db 100644 --- a/modules/project-factory/budgets.tf +++ b/modules/project-factory/budgets.tf @@ -16,13 +16,40 @@ # tfdoc:file:description Billing budget factory locals. +locals { + budget_folder_sets = flatten([ + for k, v in local.folders_input : [ + for vv in try(v.billing_budgets, []) : { + folder = k + budget = replace(vv, "$billing_budgets:", "") + } if trimspace(vv) != "" + ] + ]) + budget_project_sets = flatten([ + for k, v in local.projects_input : [ + for vv in try(v.billing_budgets, []) : { + project = k + budget = replace(vv, "$billing_budgets:", "") + } if trimspace(vv) != "" + ] + ]) +} + module "billing-budgets" { source = "../billing-account" count = var.factories_config.budgets != null ? 1 : 0 id = var.factories_config.budgets.billing_account_id context = merge(local.ctx, { - folder_ids = local.ctx.folder_ids + folder_ids = local.ctx.folder_ids + folder_sets = { + for v in local.budget_folder_sets : + v.budget => local.folder_ids[v.folder]... + } project_ids = local.ctx_project_ids + project_sets = { + for v in local.budget_project_sets : + v.budget => "projects/${local.outputs_projects[v.project].number}"... + } }) factories_config = { budgets_data_path = var.factories_config.budgets.data diff --git a/modules/project-factory/main.tf b/modules/project-factory/main.tf index 1dba2cc4a..90d85bf22 100644 --- a/modules/project-factory/main.tf +++ b/modules/project-factory/main.tf @@ -39,7 +39,7 @@ resource "terraform_data" "defaults_preconditions" { } # precondition { # condition = local.projects_input == null - # error_message = yamlencode(local.projects_input["iac-0"]) + # error_message = yamlencode(local.budget_project_sets) # } } } diff --git a/modules/project-factory/schemas/folder.schema.json b/modules/project-factory/schemas/folder.schema.json index 837e33538..907b2dff1 100644 --- a/modules/project-factory/schemas/folder.schema.json +++ b/modules/project-factory/schemas/folder.schema.json @@ -74,6 +74,12 @@ } } }, + "billing_budgets": { + "type": "array", + "items": { + "type": "string" + } + }, "contacts": { "type": "object", "additionalProperties": false, diff --git a/tests/modules/project_factory/examples/example.yaml b/tests/modules/project_factory/examples/example.yaml index 75e3736ee..5486cf544 100644 --- a/tests/modules/project_factory/examples/example.yaml +++ b/tests/modules/project_factory/examples/example.yaml @@ -96,7 +96,6 @@ values: credit_types: null credit_types_treatment: INCLUDE_ALL_CREDITS custom_period: [] - projects: null resource_ancestors: - folders/1234567890 subaccounts: null