Expose project factory stage defaults via a YAML file (#3354)

* initial implementation

* README

* tfdoc
This commit is contained in:
Ludovico Magnocavallo
2025-09-24 11:59:11 +02:00
committed by GitHub
parent 2492494c68
commit 90ee0ccf12
8 changed files with 971 additions and 209 deletions

View File

@@ -11,6 +11,7 @@
- [Factory configuration](#factory-configuration)
- [Stage provider and Terraform variables](#stage-provider-and-terraform-variables)
- [Managing folders and projects](#managing-folders-and-projects)
- [Project defaults and overrides](#project-defaults-and-overrides)
- [Folder and hierarchy management](#folder-and-hierarchy-management)
- [Folder parent-child relationship and variable substitutions](#folder-parent-child-relationship-and-variable-substitutions)
- [Project Creation](#project-creation)
@@ -55,7 +56,7 @@ The bootstrap-specific setup is reproduced here to aid using it as a starting po
#### Automation resources
The default design uses two service accounts (read-write and read-only) and a Cloud Storage folder in a pre-existing bucket, to enable this stage for Infrastructure as Code.
The default design uses two service accounts (read-write and read-only) and a Cloud Storage folder in a pre-existing bucket, to enable this stage for Infrastructure as Code. This is an example snippet that shows how to configure the org setup stage IaC project.
```yaml
# data/projects/core/iac-0.yaml
@@ -208,6 +209,12 @@ terraform apply
The YAML data files are self-explanatory and the included [schema files](./schemas/) provide a reliable framework to allow editing the sample data, or starting from scratch to implement a different pattern. This section lists some general considerations on how folder and project files work to help getting up to speed with operations.
### Project defaults and overrides
The underlying module supports a way of defining sets of values that can be used as defaults of overrides for specific project attributes. This stage supports the same, and allows setting defaults and overrides either via Terraform variables, or via a dedicated YAML defaults file.
An example defaults file is provided in the `data` folder, and the relevant schema (or the corresponding variable type) supports the full interface provided in the underlying module. Defaults from Terraform variables and the YAML file are merged, with the caveat that Where the same attribute (for example `billing_account`) is defined in both, the file takes precedence.
### Folder and hierarchy management
The project factory manages its folder hierarchy via a filesystem tree, rooted in the path defined via the `factories_config.folders` variable.
@@ -353,6 +360,7 @@ automation:
| [main.tf](./main.tf) | Project factory. | <code>project-factory</code> | |
| [outputs.tf](./outputs.tf) | Module outputs. | | <code>google_storage_bucket_object</code> |
| [variables-fast.tf](./variables-fast.tf) | None | | |
| [variables-projects.tf](./variables-projects.tf) | None | | |
| [variables.tf](./variables.tf) | Module variables. | | |
## Variables
@@ -364,10 +372,10 @@ automation:
| [prefix](variables-fast.tf#L92) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | <code>string</code> | ✓ | | <code>0-org-setup</code> |
| [context](variables.tf#L17) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; condition_vars &#61; optional&#40;map&#40;map&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; folder_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; kms_keys &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; notification_channels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_host_projects &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; vpc_sc_perimeters &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [custom_roles](variables-fast.tf#L34) | Custom roles defined at the org level, in key => id format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-org-setup</code> |
| [data_defaults](variables.tf#L36) | Optional default values used when corresponding project or folder data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; bucket &#61; optional&#40;object&#40;&#123;&#10; force_destroy &#61; optional&#40;bool&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; deletion_policy &#61; optional&#40;string&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; project_reuse &#61; optional&#40;object&#40;&#123;&#10; use_data_source &#61; optional&#40;bool, true&#41;&#10; attributes &#61; optional&#40;object&#40;&#123;&#10; name &#61; string&#10; number &#61; number&#10; services_enabled &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; shared_vpc_service_config &#61; optional&#40;object&#40;&#123;&#10; host_project &#61; string&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_agent_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_agent_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; universe &#61; optional&#40;object&#40;&#123;&#10; prefix &#61; string&#10; unavailable_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [data_merges](variables.tf#L108) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | <code title="object&#40;&#123;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [data_overrides](variables.tf#L127) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; bucket &#61; optional&#40;object&#40;&#123;&#10; force_destroy &#61; optional&#40;bool&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; deletion_policy &#61; optional&#40;string&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10; universe &#61; optional&#40;object&#40;&#123;&#10; prefix &#61; string&#10; unavailable_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [factories_config](variables.tf#L173) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; folders &#61; optional&#40;string, &#34;data&#47;folders&#34;&#41;&#10; projects &#61; optional&#40;string, &#34;data&#47;projects&#34;&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account_id &#61; string&#10; data &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [data_defaults](variables-projects.tf#L17) | Optional default values used when corresponding project or folder data from files are missing. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; bucket &#61; optional&#40;object&#40;&#123;&#10; force_destroy &#61; optional&#40;bool&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; deletion_policy &#61; optional&#40;string&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; project_reuse &#61; optional&#40;object&#40;&#123;&#10; use_data_source &#61; optional&#40;bool, true&#41;&#10; attributes &#61; optional&#40;object&#40;&#123;&#10; name &#61; string&#10; number &#61; number&#10; services_enabled &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; shared_vpc_service_config &#61; optional&#40;object&#40;&#123;&#10; host_project &#61; string&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; network_users &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_agent_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_agent_subnet_iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; service_iam_grants &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; network_subnet_users &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; universe &#61; optional&#40;object&#40;&#123;&#10; prefix &#61; string&#10; unavailable_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [data_merges](variables-projects.tf#L89) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | <code title="object&#40;&#123;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; metric_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [data_overrides](variables-projects.tf#L108) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | <code title="object&#40;&#123;&#10; billing_account &#61; optional&#40;string&#41;&#10; bucket &#61; optional&#40;object&#40;&#123;&#10; force_destroy &#61; optional&#40;bool&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; contacts &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; deletion_policy &#61; optional&#40;string&#41;&#10; factories_config &#61; optional&#40;object&#40;&#123;&#10; custom_roles &#61; optional&#40;string&#41;&#10; observability &#61; optional&#40;string&#41;&#10; org_policies &#61; optional&#40;string&#41;&#10; quotas &#61; optional&#40;string&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; parent &#61; optional&#40;string&#41;&#10; prefix &#61; optional&#40;string&#41;&#10; service_encryption_key_ids &#61; optional&#40;map&#40;list&#40;string&#41;&#41;&#41;&#10; storage_location &#61; optional&#40;string&#41;&#10; tag_bindings &#61; optional&#40;map&#40;string&#41;&#41;&#10; services &#61; optional&#40;list&#40;string&#41;&#41;&#10; service_accounts &#61; optional&#40;map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; iam_self_roles &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10; universe &#61; optional&#40;object&#40;&#123;&#10; prefix &#61; string&#10; unavailable_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; vpc_sc &#61; optional&#40;object&#40;&#123;&#10; perimeter_name &#61; string&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; logging_data_access &#61; optional&#40;map&#40;object&#40;&#123;&#10; ADMIN_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_READ &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;,&#10; DATA_WRITE &#61; optional&#40;object&#40;&#123; exempted_members &#61; optional&#40;list&#40;string&#41;&#41; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [factories_config](variables.tf#L36) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; defaults &#61; optional&#40;string, &#34;data&#47;defaults.yaml&#34;&#41;&#10; folders &#61; optional&#40;string, &#34;data&#47;folders&#34;&#41;&#10; projects &#61; optional&#40;string, &#34;data&#47;projects&#34;&#41;&#10; budgets &#61; optional&#40;object&#40;&#123;&#10; billing_account_id &#61; string&#10; data &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | |
| [folder_ids](variables-fast.tf#L42) | Folders created in the bootstrap stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-org-setup</code> |
| [host_project_ids](variables-fast.tf#L58) | Host project for the shared VPC. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>2-networking</code> |
| [iam_principals](variables-fast.tf#L50) | IAM-format principals. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-org-setup</code> |
@@ -376,7 +384,7 @@ automation:
| [perimeters](variables-fast.tf#L84) | Optional VPC-SC perimeter ids. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>1-vpcsc</code> |
| [project_ids](variables-fast.tf#L102) | Projects created in the bootstrap stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-org-setup</code> |
| [service_accounts](variables-fast.tf#L110) | Service accounts created in the bootstrap stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-org-setup</code> |
| [stage_name](variables.tf#L193) | FAST stage name. Used to separate output files across different factories. | <code>string</code> | | <code>&#34;2-project-factory&#34;</code> | |
| [stage_name](variables.tf#L57) | FAST stage name. Used to separate output files across different factories. | <code>string</code> | | <code>&#34;2-project-factory&#34;</code> | |
| [subnet_self_links](variables-fast.tf#L118) | Shared VPC subnet IDs. | <code>map&#40;map&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | <code>2-networking</code> |
| [tag_values](variables-fast.tf#L126) | FAST-managed resource manager tag values. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> | <code>0-org-setup</code> |

View File

@@ -0,0 +1,33 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# yaml-language-server: $schema=../schemas/defaults.schema.json
# environment-specific project defaults, merges and overrides can be defined here
projects:
# defaults:
# storage_location: europe-west8
merges:
services:
- logging.googleapis.com
- monitoring.googleapis.com
# overrides:
# prefix: tf-playground
# environment-specific static contexts can be defined here
# context:
# iam_principals:
# foo: group:foo@example.com

View File

@@ -17,6 +17,38 @@
# tfdoc:file:description Project factory.
locals {
_defaults = yamldecode(file(pathexpand(var.factories_config.defaults)))
context = merge(var.context, lookup(local._defaults, "context", {}))
fast_defaults = {
billing_account = coalesce(
var.data_defaults.billing_account,
var.billing_account.id
)
prefix = coalesce(
var.data_defaults.prefix, var.prefix
)
storage_location = coalesce(
var.data_defaults.storage_location, var.locations.storage
)
}
project_defaults = {
defaults = {
for k, v in var.data_defaults : k => try(
local._defaults.projects.defaults[k],
lookup(local.fast_defaults, k, v)
)
}
merges = {
for k, v in var.data_merges : k => try(
local._defaults.projects.merges[k], v
)
}
overrides = {
for k, v in var.data_overrides : k => try(
local._defaults.projects.overrides[k], v
)
}
}
subnet_self_links = flatten([
for net, subnets in var.subnet_self_links : [
for subnet_name, subnet_link in subnets : {
@@ -34,55 +66,29 @@ module "factory" {
subnet_self_links = {
for v in local.subnet_self_links : v.key => v.link
}
}, var.context.condition_vars)
custom_roles = merge(
var.custom_roles, var.context.custom_roles
)
folder_ids = merge(
var.folder_ids, var.context.folder_ids
)
}, local.context.condition_vars)
custom_roles = merge(var.custom_roles, local.context.custom_roles)
folder_ids = merge(var.folder_ids, local.context.folder_ids)
iam_principals = merge(
var.iam_principals,
{
for k, v in var.service_accounts :
k => "serviceAccount:${v}" if v != null
},
var.context.iam_principals
local.context.iam_principals
)
kms_keys = merge(
var.kms_keys, var.context.kms_keys
)
locations = merge(
var.locations, var.context.locations
)
notification_channels = var.context.notification_channels
kms_keys = merge(var.kms_keys, local.context.kms_keys)
locations = merge(var.locations, local.context.locations)
notification_channels = local.context.notification_channels
project_ids = merge(
var.project_ids, var.host_project_ids, var.context.project_ids
)
tag_values = merge(
var.tag_values, var.context.tag_values
)
vpc_sc_perimeters = merge(
var.perimeters, var.context.vpc_sc_perimeters
var.project_ids, var.host_project_ids, local.context.project_ids
)
tag_values = merge(var.tag_values, local.context.tag_values)
vpc_sc_perimeters = merge(var.perimeters, local.context.vpc_sc_perimeters)
}
data_defaults = merge(var.data_defaults, {
billing_account = coalesce(
var.data_defaults.billing_account, var.billing_account.id
)
prefix = coalesce(var.data_defaults.prefix, var.prefix)
storage_location = coalesce(
var.data_defaults.storage_location, var.locations.storage
)
})
data_merges = merge(var.data_merges, {
services = length(var.data_merges.services) > 0 ? var.data_merges.services : [
"logging.googleapis.com",
"monitoring.googleapis.com"
]
}
)
data_overrides = var.data_overrides
data_defaults = local.project_defaults.defaults
data_merges = local.project_defaults.merges
data_overrides = local.project_defaults.overrides
factories_config = merge(var.factories_config, {
budgets = {
billing_account_id = try(

View File

@@ -0,0 +1,700 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Bootstrap Defaults",
"type": "object",
"additionalProperties": false,
"properties": {
"projects": {
"type": "object",
"additionalProperties": false,
"properties": {
"defaults": {
"type": "object",
"additionalProperties": false,
"properties": {
"billing_account": {
"type": "string"
},
"bucket": {
"type": "object",
"additionalProperties": false,
"properties": {
"force_destroy": {
"type": "boolean"
}
}
},
"contacts": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"deletion_policy": {
"type": "string",
"enum": [
"PREVENT",
"DELETE",
"ABANDON"
]
},
"labels": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"logging_data_access": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"properties": {
"ADMIN_READ": {
"type": "object",
"properties": {
"exempted_members": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"DATA_READ": {
"type": "object",
"properties": {
"exempted_members": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"DATA_WRITE": {
"type": "object",
"properties": {
"exempted_members": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"metric_scopes": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"parent": {
"type": "string"
},
"prefix": {
"type": "string"
},
"project_reuse": {
"type": "object",
"additionalProperties": false,
"properties": {
"use_data_source": {
"type": "boolean",
"default": true
},
"attributes": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"number": {
"type": "number"
},
"services_enabled": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
}
},
"required": [
"name",
"number"
]
}
}
},
"service_accounts": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"properties": {
"display_name": {
"type": "string",
"default": "Terraform-managed."
},
"iam_self_roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"service_encryption_key_ids": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"services": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"shared_vpc_service_config": {
"type": "object",
"additionalProperties": false,
"properties": {
"host_project": {
"type": "string"
},
"iam_bindings_additive": {
"$ref": "#/$defs/iam_bindings_additive"
},
"network_users": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"service_agent_iam": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"service_agent_subnet_iam": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"service_iam_grants": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"network_subnet_users": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"required": [
"host_project"
]
},
"storage_location": {
"type": "string"
},
"tag_bindings": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "string"
}
},
"universe": {
"type": "object",
"additionalProperties": false,
"required": [
"prefix"
],
"properties": {
"prefix": {
"type": "string"
},
"unavailable_service_identities": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"vpc_sc": {
"type": "object",
"properties": {
"perimeter_name": {
"type": "string"
},
"is_dry_run": {
"type": "boolean",
"default": false
}
},
"required": [
"perimeter_name"
]
}
}
},
"merges": {
"type": "object",
"additionalProperties": false,
"properties": {
"contacts": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"labels": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"metric_scopes": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"service_encryption_key_ids": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"service_accounts": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"properties": {
"display_name": {
"type": "string",
"default": "Terraform-managed."
},
"iam_self_roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"services": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
}
}
},
"overrides": {
"type": "object",
"additionalProperties": false,
"properties": {
"billing_account": {
"type": "string"
},
"bucket": {
"type": "object",
"additionalProperties": false,
"properties": {
"force_destroy": {
"type": "boolean"
}
}
},
"contacts": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"deletion_policy": {
"type": "string",
"enum": [
"PREVENT",
"DELETE",
"ABANDON"
]
},
"logging_data_access": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"properties": {
"ADMIN_READ": {
"type": "object",
"properties": {
"exempted_members": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"DATA_READ": {
"type": "object",
"properties": {
"exempted_members": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"DATA_WRITE": {
"type": "object",
"properties": {
"exempted_members": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"parent": {
"type": "string"
},
"prefix": {
"type": "string"
},
"service_accounts": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"properties": {
"display_name": {
"type": "string",
"default": "Terraform-managed."
},
"iam_self_roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"service_encryption_key_ids": {
"type": "object",
"default": {},
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"services": {
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"storage_location": {
"type": "string"
},
"tag_bindings": {
"type": "object",
"default": {},
"additionalProperties": {
"type": "string"
}
},
"universe": {
"type": "object",
"additionalProperties": false,
"required": [
"prefix"
],
"properties": {
"prefix": {
"type": "string"
},
"unavailable_service_identities": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"vpc_sc": {
"type": "object",
"properties": {
"perimeter_name": {
"type": "string"
},
"is_dry_run": {
"type": "boolean",
"default": false
}
},
"required": [
"perimeter_name"
]
}
}
}
}
},
"context": {
"type": "object",
"additionalProperties": false,
"properties": {
"iam_principals": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"output_files": {
"type": "object",
"additionalProperties": false,
"properties": {
"local_path": {
"type": "string"
},
"storage_bucket": {
"type": "string"
},
"providers": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9][a-z0-9_-]+$": {
"type": "object",
"additionalProperties": false,
"required": [
"bucket",
"service_account"
],
"properties": {
"bucket": {
"type": "string"
},
"prefix": {
"type": "string"
},
"service_account": {
"type": "string"
}
}
}
}
}
}
}
},
"$defs": {
"iam": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^(?:roles/|\\$custom_roles:)": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:||\\$iam_principals:[a-z0-9_-]+)"
}
}
}
},
"iam_bindings": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"members": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:[a-z0-9_-]+)"
}
},
"role": {
"type": "string",
"pattern": "^(?:roles/|\\$custom_roles:)"
},
"condition": {
"type": "object",
"additionalProperties": false,
"required": [
"expression",
"title"
],
"properties": {
"expression": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
}
}
}
},
"iam_bindings_additive": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"member": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:[a-z0-9_-]+)"
},
"role": {
"type": "string",
"pattern": "^(?:roles/|\\$custom_roles:)"
},
"condition": {
"type": "object",
"additionalProperties": false,
"required": [
"expression",
"title"
],
"properties": {
"expression": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
}
}
}
},
"iam_by_principals": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:[a-z0-9_-]+)": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:roles/|\\$custom_roles:)"
}
}
}
}
}
}

View File

@@ -0,0 +1,153 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "data_defaults" {
description = "Optional default values used when corresponding project or folder data from files are missing."
type = object({
billing_account = optional(string)
bucket = optional(object({
force_destroy = optional(bool)
}), {})
contacts = optional(map(list(string)), {})
deletion_policy = optional(string)
factories_config = optional(object({
custom_roles = optional(string)
observability = optional(string)
org_policies = optional(string)
quotas = optional(string)
}), {})
labels = optional(map(string), {})
metric_scopes = optional(list(string), [])
parent = optional(string)
prefix = optional(string)
project_reuse = optional(object({
use_data_source = optional(bool, true)
attributes = optional(object({
name = string
number = number
services_enabled = optional(list(string), [])
}))
}))
service_encryption_key_ids = optional(map(list(string)), {})
services = optional(list(string), [])
shared_vpc_service_config = optional(object({
host_project = string
iam_bindings_additive = optional(map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
})), {})
network_users = optional(list(string), [])
service_agent_iam = optional(map(list(string)), {})
service_agent_subnet_iam = optional(map(list(string)), {})
service_iam_grants = optional(list(string), [])
network_subnet_users = optional(map(list(string)), {})
}))
storage_location = optional(string)
tag_bindings = optional(map(string), {})
# non-project resources
service_accounts = optional(map(object({
display_name = optional(string, "Terraform-managed.")
iam_self_roles = optional(list(string))
})), {})
universe = optional(object({
prefix = string
unavailable_service_identities = optional(list(string), [])
unavailable_services = optional(list(string), [])
}))
vpc_sc = optional(object({
perimeter_name = string
is_dry_run = optional(bool, false)
}))
logging_data_access = optional(map(object({
ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
})), {})
})
nullable = false
default = {}
}
variable "data_merges" {
description = "Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`."
type = object({
contacts = optional(map(list(string)), {})
labels = optional(map(string), {})
metric_scopes = optional(list(string), [])
service_encryption_key_ids = optional(map(list(string)), {})
services = optional(list(string), [])
tag_bindings = optional(map(string), {})
# non-project resources
service_accounts = optional(map(object({
display_name = optional(string, "Terraform-managed.")
iam_self_roles = optional(list(string))
})), {})
})
nullable = false
default = {}
}
variable "data_overrides" {
description = "Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`."
type = object({
# data overrides default to null to mark that they should not override
billing_account = optional(string)
bucket = optional(object({
force_destroy = optional(bool)
}), {})
contacts = optional(map(list(string)))
deletion_policy = optional(string)
factories_config = optional(object({
custom_roles = optional(string)
observability = optional(string)
org_policies = optional(string)
quotas = optional(string)
}), {})
parent = optional(string)
prefix = optional(string)
service_encryption_key_ids = optional(map(list(string)))
storage_location = optional(string)
tag_bindings = optional(map(string))
services = optional(list(string))
# non-project resources
service_accounts = optional(map(object({
display_name = optional(string, "Terraform-managed.")
iam_self_roles = optional(list(string))
})))
universe = optional(object({
prefix = string
unavailable_service_identities = optional(list(string), [])
unavailable_services = optional(list(string), [])
}))
vpc_sc = optional(object({
perimeter_name = string
is_dry_run = optional(bool, false)
}))
logging_data_access = optional(map(object({
ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
})))
})
nullable = false
default = {}
}

View File

@@ -33,146 +33,10 @@ variable "context" {
nullable = false
}
variable "data_defaults" {
description = "Optional default values used when corresponding project or folder data from files are missing."
type = object({
billing_account = optional(string)
bucket = optional(object({
force_destroy = optional(bool)
}), {})
contacts = optional(map(list(string)), {})
deletion_policy = optional(string)
factories_config = optional(object({
custom_roles = optional(string)
observability = optional(string)
org_policies = optional(string)
quotas = optional(string)
}), {})
labels = optional(map(string), {})
metric_scopes = optional(list(string), [])
parent = optional(string)
prefix = optional(string)
project_reuse = optional(object({
use_data_source = optional(bool, true)
attributes = optional(object({
name = string
number = number
services_enabled = optional(list(string), [])
}))
}))
service_encryption_key_ids = optional(map(list(string)), {})
services = optional(list(string), [])
shared_vpc_service_config = optional(object({
host_project = string
iam_bindings_additive = optional(map(object({
member = string
role = string
condition = optional(object({
expression = string
title = string
description = optional(string)
}))
})), {})
network_users = optional(list(string), [])
service_agent_iam = optional(map(list(string)), {})
service_agent_subnet_iam = optional(map(list(string)), {})
service_iam_grants = optional(list(string), [])
network_subnet_users = optional(map(list(string)), {})
}))
storage_location = optional(string)
tag_bindings = optional(map(string), {})
# non-project resources
service_accounts = optional(map(object({
display_name = optional(string, "Terraform-managed.")
iam_self_roles = optional(list(string))
})), {})
universe = optional(object({
prefix = string
unavailable_service_identities = optional(list(string), [])
unavailable_services = optional(list(string), [])
}))
vpc_sc = optional(object({
perimeter_name = string
is_dry_run = optional(bool, false)
}))
logging_data_access = optional(map(object({
ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
})), {})
})
nullable = false
default = {}
}
variable "data_merges" {
description = "Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`."
type = object({
contacts = optional(map(list(string)), {})
labels = optional(map(string), {})
metric_scopes = optional(list(string), [])
service_encryption_key_ids = optional(map(list(string)), {})
services = optional(list(string), [])
tag_bindings = optional(map(string), {})
# non-project resources
service_accounts = optional(map(object({
display_name = optional(string, "Terraform-managed.")
iam_self_roles = optional(list(string))
})), {})
})
nullable = false
default = {}
}
variable "data_overrides" {
description = "Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`."
type = object({
# data overrides default to null to mark that they should not override
billing_account = optional(string)
bucket = optional(object({
force_destroy = optional(bool)
}), {})
contacts = optional(map(list(string)))
deletion_policy = optional(string)
factories_config = optional(object({
custom_roles = optional(string)
observability = optional(string)
org_policies = optional(string)
quotas = optional(string)
}), {})
parent = optional(string)
prefix = optional(string)
service_encryption_key_ids = optional(map(list(string)))
storage_location = optional(string)
tag_bindings = optional(map(string))
services = optional(list(string))
# non-project resources
service_accounts = optional(map(object({
display_name = optional(string, "Terraform-managed.")
iam_self_roles = optional(list(string))
})))
universe = optional(object({
prefix = string
unavailable_service_identities = optional(list(string), [])
unavailable_services = optional(list(string), [])
}))
vpc_sc = optional(object({
perimeter_name = string
is_dry_run = optional(bool, false)
}))
logging_data_access = optional(map(object({
ADMIN_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_READ = optional(object({ exempted_members = optional(list(string)) })),
DATA_WRITE = optional(object({ exempted_members = optional(list(string)) }))
})))
})
nullable = false
default = {}
}
variable "factories_config" {
description = "Path to folder with YAML resource description data files."
type = object({
defaults = optional(string, "data/defaults.yaml")
folders = optional(string, "data/folders")
projects = optional(string, "data/projects")
budgets = optional(object({