diff --git a/fast/stages/1-resman/README.md b/fast/stages/1-resman/README.md index ee83f1ff1..f32f4e262 100644 --- a/fast/stages/1-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -18,7 +18,7 @@ The following diagram is a high level reference of the resources created and man - [Top-level folders](#top-level-folders) - [Stage 2](#stage-2) - [Stage 3](#stage-3) - - [Project (and hierarchy) factory](#project-and-hierarchy-factory) + - [Project and hierarchy factory](#project-and-hierarchy-factory) - [Other design considerations](#other-design-considerations) - [Secure tags](#secure-tags) - [Multitenancy](#multitenancy) @@ -59,62 +59,60 @@ This stage allows a certain degree of free-form hierarchy design on top of inste Top-level folders, as indicated by their name, are folders directly attached to the organization that can be freely defined via Terraform variables or factory YAML files. They represent a node in the organization, which can be used to partition the hierarchy via IAM or tag bindings, and to implement separate automation stages via their optional IaC resources. -Top-level folders support the full interface of the [folder module](../../../modules/folder/), and can fit in the FAST design in different ways: +Top-level folders offer less direct integration into the FAST workflow and machinery, and are meant to solve specific use cases in addition to our standard stage 2 and 3 described in the following section. + +The full interface of the [folder module](../../../modules/folder/) is supported for top-level folders, allowing them to fit in the FAST design in different ways: - as supporting folders for the project factory, by granting high level permissions to its service accounts via IAM and tag bindings (see the ["Teams" example in the data folder](./data/top-level-folders/teams.yaml)) -- as standalone folders to support custom usage, with or without associated IaC resources (see the ["Sandbox" exanple in the data folder](./data/top-level-folders/sandbox.yaml)) +- as standalone folders for custom usage, with or without associated IaC resources (see the ["Sandbox" exanple in the data folder](./data/top-level-folders/sandbox.yaml)) - as grouping nodes for the environment-specific stage 3 folders (see the ["GCVE" example in the data folder](./data/top-level-folders/gcve.yaml)) -- as a grouping node for stage 2s, for example via a "Shared Services" top-level folder set as the `folder_config.parent_id` attribute for networking and security stages +- as grouping nodes for stage 2s, for example via a "Shared Services" top-level folder set as the `folder_config.parent_id` attribute for networking and security stages Top-level folders support context-based expansion for service accounts and organization-level tags, which can be referenced by name (e.g. `project-factory` to refer to the project factory service accounts). This allows writing portable organization-independent YAML that can be shared across different FAST installations. ### Stage 2 -FAST stage 2s implement core infrastructure services shared across the organization. In the FAST design networking, security, network security and the project factory are defined as stage 2. +FAST stage 2s implement core infrastructure services shared across the organization. In the FAST design networking, security, and the project factory are defined as stage 2. Their interface is sufficiently flexible to allow easy definition of custom stages, which can then be integrated in the framework. FAST stage 2s are typically managed by dedicated teams, they implement environment separation internally due to the complexity of their designs, and provide resources and specific IAM permissions to other shared services implemented as stage 3s (e.g. Shared VPC networks, IAM delegated grants on host projects/subnets or KMS keys). -The default configuration enables all stage 2s except network security. Each stage can be customized via a set of variable-level attributes: +The default configuration enables all stage 2s via factory files in the `data/stage-2` folder. Each stage can be customized via a set of variable-level attributes: -- `short_name` defines the name used for the stage IaC buckets and service accounts -- `cicd_config` turns on CI/CD configuration and generates the workflow file for the stage -- `folder_config` controls whether environment-level folders are created under the stage main folder (e.g. `Networking/Development`), allows defining additional IAM bindings on the main folder, or changing its name and parent +- `short_name` defines the name used for the stage IaC resources +- `cicd_config` optionally configures built-in CI/CD support for the stage +- `folder_config` controls the name, organization policies, and IAM profile for the stage folder, and allows defining additional environment-level subfolders +- `organization_config` controls the IAM profile for the stage at the organization level +- `stage3_config` allows defining signals that are passed on to the stage via output variables, on specific IAM configurations needed by stage 3s -Folder configuration is only available for networking and security stages, as the project factory and network security stages are "folderless", using top-level folders or organization-level resources. +Each stage creates its own tag value in the `context` key, which can then be used for conditional roles at the organization level (`context/networking`, `context/project-factory` etc.) when needed. The tag value is assigned to the stage's folder, and can be applied to other folders to enable specific functionality, for example to allow the project factory to manage additional top-level folders. -Each stage creates its own tag value in the `context` key, which is used by FAST for conditional roles at the organization level (`context/networking`, `context/project-factory` etc.). The tag value is assigned to the stage's folder, and can be applied to other folders to enable specific functionality, as described further down for the project factory. - -Think of stage 2s as "named stages" which have specific ties and privileges on the organization. Due to their complexity and the potential need for custom changes, they are implemented in code via dedicated terraform resources each in a stage file (e.g. `stage2-networking.tf`). +Think of stage 2s as "named stages" which can define specific IAM configurations on the organization, and are free to define their own environment-level constraints. ### Stage 3 -FAST stage 3s are designed to host shared infrastructure that leverages core services from stage 2 (networking, encryption keys, etc.), has limited access to the organization, and is partitioned (or "cloned") by environment. +FAST stage 3s are designed to host shared infrastructure that leverages core services from stage 2 (networking, encryption keys, etc.), and is partitioned by environment and subject to environment-level constraints, with no direct access to organization-level IAM configurations. -As shared services they are still managed by dedicated teams, but principals and permissions might differ between environments. Most stage 3s leverage folders (environment-level project factories are the exception), where the stage root folder is created via top-level folders configuration, and the lower level environment folders are part of the stage. +As shared services they are still managed by dedicated teams, but principals and permissions might differ between environments. Stage 3s typically leverage top-level folders, under which the environment-level folders for the stage are then created. Configuration can be done either via Terraform variables or factory YAML files. The second option is used by default, providing a set of factory files for top-level folders and stage 3s that mirror the legacy FAST hierarchy implemented via code. -Stage 3 configuration is similar to the stage 2 one described above except for a few differences. Each stage defined in the `fast_stage_3` map: +Configuration is similar to the stage 2 one described above, save that stage 3: -- can define an arbitrary name in the map key, which is used for the stage's output files and internal context-based substitutions -- needs to define an environment which is present in the bootstrap `environment_names` definition -- can define organization-level IAM bindings that are conditional to the stage tag value, or an arbitrary one defined in configuration -- can define stage 2-level tag bindings that are effective only on the stage 2 resources matching the same environment +- need to define the environment for which they will be deployed +- have no way to configure organization-level IAM -> TODO: examples from data, make sure the add IAM for GCVE etc. there - -### Project (and hierarchy) factory +### Project and hierarchy factory Despite being itself a stage 2 (and potentially one or more environment-specific stage 3), the project factory is an important primitive to shape the lower level resource hierarchy which implements folder and project management. -By default FAST offers a single organization-wide project factory with the following characteristics: +By default FAST configures a single organization-wide project factory with the following characteristics: - any top-level folder with the suitable set of roles can be managed as a sub-hierarchy tree by the project factory (see the ["Teams" definition](./data/top-level-folders/teams.yaml) in the data folder) - organization policy management on its folders and projects by the project factory only requires binding the `context/project-factory` tag value - networking-related project configuration is available by default, the project factory can grant a limited set of roles on network resources, and attach service projects to VPC host projects - security-related project configuration is available by default, the project factory can grant the KMS encrypt/decrypt role on centralized KMS key in the security stage -If environment-specific project factories are desirable, they can be configured as stage 3 as the examples in the stage3 data folder show. +Additional project factories can of course be defined by cloning the default stage 2 configuration, and changing the stage 2 names and folders. ## Other design considerations @@ -244,16 +242,13 @@ terraform apply | name | description | modules | resources | |---|---|---|---| | [billing.tf](./billing.tf) | Billing resources for external billing use cases. | | google_billing_account_iam_member | -| [iam.tf](./iam.tf) | Organization or root node-level IAM bindings. | | | | [main.tf](./main.tf) | Module-level locals and resources. | | | | [organization.tf](./organization.tf) | Organization policies. | organization | | | [outputs-cicd.tf](./outputs-cicd.tf) | Locals for CI/CD workflow files. | | | | [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | google_storage_bucket_object · local_file | | [outputs-providers.tf](./outputs-providers.tf) | Locals for provider output files. | | | | [outputs.tf](./outputs.tf) | Module outputs. | | | -| [stage-2-networking.tf](./stage-2-networking.tf) | None | folder · gcs · iam-service-account | | -| [stage-2-project-factory.tf](./stage-2-project-factory.tf) | None | gcs · iam-service-account | | -| [stage-2-security.tf](./stage-2-security.tf) | None | folder · gcs · iam-service-account | | +| [stage-2.tf](./stage-2.tf) | Stage 2s locals and resources. | folder · gcs · iam-service-account | | | [stage-3.tf](./stage-3.tf) | None | folder · gcs · iam-service-account | | | [stage-cicd.tf](./stage-cicd.tf) | CI/CD locals and resources. | iam-service-account | | | [tenant-logging.tf](./tenant-logging.tf) | Audit log project and sink for tenant root folder. | bigquery-dataset · gcs · logging-bucket · pubsub | | @@ -276,25 +271,26 @@ terraform apply | [organization](variables-fast.tf#L131) | Organization details. | object({…}) | ✓ | | 0-bootstrap | | [prefix](variables-fast.tf#L149) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | | [custom_roles](variables-fast.tf#L54) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | -| [factories_config](variables.tf#L20) | Configuration for the resource factories or external data. | object({…}) | | {} | | +| [factories_config](variables.tf#L20) | Configuration for the resource factories or external data. | object({…}) | | {} | | | [fast_addon](variables-addons.tf#L17) | FAST addons configurations for stages 2. Keys are used as short names for the add-on resources. | map(object({…})) | | {} | | -| [fast_stage_2](variables-stages.tf#L17) | FAST stages 2 configurations. | object({…}) | | {} | | -| [fast_stage_3](variables-stages.tf#L83) | FAST stages 3 configurations. | map(object({…})) | | {} | | +| [fast_stage_2](variables-stages.tf#L17) | FAST stages 2 configurations. | map(object({…})) | | {} | | +| [fast_stage_3](variables-stages.tf#L104) | FAST stages 3 configurations. | map(object({…})) | | {} | | | [groups](variables-fast.tf#L90) | Group names or IAM-format principals to grant organization-level permissions. If just the name is provided, the 'group:' principal and organization domain are interpolated. | object({…}) | | {} | 0-bootstrap | | [locations](variables-fast.tf#L105) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | 0-bootstrap | | [outputs_location](variables.tf#L31) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [resource_names](variables.tf#L37) | Resource names overrides for specific resources. Stage names are interpolated via `$${name}`. Prefix is always set via code, except where noted in the variable type. | object({…}) | | {} | | +| [resource_names](variables.tf#L37) | Resource names overrides for specific resources. Stage names are interpolated via `$${name}`. Prefix is always set via code, except where noted in the variable type. | object({…}) | | {} | | | [root_node](variables-fast.tf#L155) | Root node for the hierarchy, if running in tenant mode. | string | | null | 0-bootstrap | -| [tag_names](variables.tf#L62) | Customized names for resource management tags. | object({…}) | | {} | | -| [tags](variables.tf#L76) | Custom secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | +| [tag_names](variables.tf#L57) | Customized names for resource management tags. | object({…}) | | {} | | +| [tags](variables.tf#L71) | Custom secure tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | | [top_level_folders](variables-toplevel-folders.tf#L17) | Additional top-level folders. Keys are used for service account and bucket names, values implement the folders module interface with the addition of the 'automation' attribute. | map(object({…})) | | {} | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [cicd_repositories](outputs.tf#L74) | WIF configuration for CI/CD repositories. | | | -| [folder_ids](outputs.tf#L86) | Folder ids. | | | -| [providers](outputs.tf#L92) | Terraform provider files for this stage and dependent stages. | ✓ | | -| [tfvars](outputs.tf#L99) | Terraform variable files for the following stages. | ✓ | | +| [cicd_repositories](outputs.tf#L65) | WIF configuration for CI/CD repositories. | | | +| [folder_ids](outputs.tf#L77) | Folder ids. | | | +| [providers](outputs.tf#L83) | Terraform provider files for this stage and dependent stages. | ✓ | | +| [service_accounts](outputs.tf#L89) | Service accounts. | | | +| [tfvars](outputs.tf#L95) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/1-resman/billing.tf b/fast/stages/1-resman/billing.tf index 9fb6734e4..d9007b425 100644 --- a/fast/stages/1-resman/billing.tf +++ b/fast/stages/1-resman/billing.tf @@ -19,36 +19,18 @@ locals { billing_iam = merge( # stage 2 - var.fast_stage_2.networking.enabled != true ? {} : { - sa_net_billing = { - member = module.net-sa-rw[0].iam_email + { + for k, v in local.stage2 : "sa_${v.short_name}_billing" => { + member = module.stage2-sa-rw[k].iam_email role = "roles/billing.user" } }, - var.fast_stage_2.security.enabled != true ? {} : { - sa_sec_billing = { - member = module.sec-sa-rw[0].iam_email - role = "roles/billing.user" + { + for k, v in local.stage2 : "sa_${v.short_name}_costs_manager" => { + member = module.stage2-sa-rw[k].iam_email + role = "roles/billing.costsManager" } }, - var.fast_stage_2.project_factory.enabled != true ? {} : merge( - { - sa_pf_billing = { - member = module.pf-sa-rw[0].iam_email - role = "roles/billing.user" - }, - sa_pf_costs_manager = { - member = module.pf-sa-rw[0].iam_email - role = "roles/billing.costsManager" - } - }, - var.billing_account.is_org_level != true ? {} : { - sa_pf_ro_viewer = { - member = module.pf-sa-ro[0].iam_email - role = var.custom_roles.billing_viewer - } - } - ), # stage 3 { for k, v in local.stage3 : k => { diff --git a/fast/stages/1-resman/data/stage-2/networking.yaml b/fast/stages/1-resman/data/stage-2/networking.yaml new file mode 100644 index 000000000..24b472de1 --- /dev/null +++ b/fast/stages/1-resman/data/stage-2/networking.yaml @@ -0,0 +1,73 @@ +# 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/fast-stage2.schema.json + +short_name: net +folder_config: + name: Networking + create_env_folders: true + iam_by_principals: + rw: + - roles/logging.admin + - roles/owner + - roles/resourcemanager.folderAdmin + - roles/resourcemanager.projectCreator + - roles/compute.xpnAdmin + - roles/resourcemanager.tagUser + ro: + - roles/viewer + - roles/resourcemanager.folderViewer + - roles/resourcemanager.tagViewer + project-factory-rw: + - service_project_network_admin + project-factory-ro: + - roles/compute.networkViewer + - project_iam_viewer + gcp-network-admins: + - roles/editor + # project factory delegated IAM grant + iam_bindings: + project_factory: + role: roles/resourcemanager.projectIamAdmin + members: + - project-factory-rw + condition: + title: Project factory delegated IAM grant. + expression: | + api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([ + 'roles/compute.networkUser', 'roles/composer.sharedVpcAgent', + 'roles/container.hostServiceAgentUser', 'roles/vpcaccess.user' + ]) + # iam_bindings_additive for stage 3 are added here when needed + # refer to each stage 3 documentation for snippets and examples +organization_config: + iam_bindings_additive: + sa_net_rw_fw_policy_admin: + member: rw + role: roles/compute.orgFirewallPolicyAdmin + sa_net_rw_ngfw_enterprise_admin: + member: rw + role: ngfw_enterprise_admin + sa_net_rw_xpn_admin: + member: rw + role: roles/compute.xpnAdmin + sa_net_ro_fw_policy_user: + member: ro + role: roles/compute.orgFirewallPolicyUser + sa_net_ro_ngfw_enterprise_viewer: + member: ro + role: ngfw_enterprise_viewer +# stage_3_config for IAM delegation are added here when needed +# refer to each stage 3 documentation for snippets and examples diff --git a/fast/stages/1-resman/data/stage-3/project-factory-dev.yaml b/fast/stages/1-resman/data/stage-2/project-factory.yaml similarity index 57% rename from fast/stages/1-resman/data/stage-3/project-factory-dev.yaml rename to fast/stages/1-resman/data/stage-2/project-factory.yaml index e99aa60d1..bf4e001a2 100644 --- a/fast/stages/1-resman/data/stage-3/project-factory-dev.yaml +++ b/fast/stages/1-resman/data/stage-2/project-factory.yaml @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -# yaml-language-server: $schema=../../schemas/fast-stage3.schema.json +# yaml-language-server: $schema=../../schemas/fast-stage2.schema.json short_name: pf -environment: dev -stage2_iam: - networking: - iam_admin_delegated: true - security: - iam_admin_delegated: true +organization_config: + iam_bindings_additive: + sa_pf_conditional_org_policy: + member: rw + role: roles/orgpolicy.policyAdmin + condition: + title: org_policy_tag_pf_scoped + description: Org policy tag scoped grant for project factory. + expression: | + resource.matchTag('${organization.id}/${tag_names.context}', 'project-factory') diff --git a/fast/stages/1-resman/data/stage-2/security.yaml b/fast/stages/1-resman/data/stage-2/security.yaml new file mode 100644 index 000000000..b0c42fc2c --- /dev/null +++ b/fast/stages/1-resman/data/stage-2/security.yaml @@ -0,0 +1,55 @@ +# 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/fast-stage2.schema.json + +short_name: sec +folder_config: + name: Security + iam_by_principals: + rw: + - roles/logging.admin + - roles/owner + - roles/resourcemanager.folderAdmin + - roles/resourcemanager.projectCreator + - roles/resourcemanager.tagUser + ro: + - roles/viewer + - roles/resourcemanager.folderViewer + - roles/resourcemanager.tagViewer + project-factory-rw: + - roles/cloudkms.cryptoKeyEncrypterDecrypter + project-factory-ro: + - roles/cloudkms.viewer + - project_iam_viewer + gcp-security-admins: + - roles/editor + + # project factory delegated IAM grant + iam_bindings: + project_factory: + role: roles/resourcemanager.projectIamAdmin + members: + - project-factory-rw + condition: + title: Project factory delegated IAM grant. + expression: | + api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([ + 'roles/cloudkms.cryptoKeyEncrypterDecrypter' + ]) +organization_config: + iam_bindings_additive: + sa_sec_cloudasset: + member: rw + role: roles/cloudasset.viewer diff --git a/fast/stages/1-resman/data/stage-3/gcve-dev.yaml b/fast/stages/1-resman/data/stage-3/gcve-dev.yaml index e36796dee..02e830741 100644 --- a/fast/stages/1-resman/data/stage-3/gcve-dev.yaml +++ b/fast/stages/1-resman/data/stage-3/gcve-dev.yaml @@ -19,11 +19,3 @@ environment: dev folder_config: name: Development parent_id: gcve -stage2_iam: - networking: - iam_admin_delegated: true - sa_roles: - ro: - - gcve_network_viewer - rw: - - gcve_network_admin \ No newline at end of file diff --git a/fast/stages/1-resman/data/stage-3/gcve-prod.yaml b/fast/stages/1-resman/data/stage-3/gcve-prod.yaml deleted file mode 100644 index 789064090..000000000 --- a/fast/stages/1-resman/data/stage-3/gcve-prod.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2024 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/fast-stage3.schema.json - -short_name: gcve -environment: prod -folder_config: - name: Production - parent_id: gcve -stage2_iam: - networking: - iam_admin_delegated: true - sa_roles: - ro: - - gcve_network_viewer - rw: - - gcve_network_admin \ No newline at end of file diff --git a/fast/stages/1-resman/data/stage-3/gke-dev.yaml b/fast/stages/1-resman/data/stage-3/gke-dev.yaml index c15fdb417..26caecf43 100644 --- a/fast/stages/1-resman/data/stage-3/gke-dev.yaml +++ b/fast/stages/1-resman/data/stage-3/gke-dev.yaml @@ -19,11 +19,3 @@ environment: dev folder_config: name: Development parent_id: gke -stage2_iam: - networking: - iam_admin_delegated: true - sa_roles: - ro: - - roles/dns.reader - rw: - - roles/dns.admin diff --git a/fast/stages/1-resman/data/stage-3/gke-prod.yaml b/fast/stages/1-resman/data/stage-3/gke-prod.yaml deleted file mode 100644 index 97b5396a9..000000000 --- a/fast/stages/1-resman/data/stage-3/gke-prod.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2024 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/fast-stage3.schema.json - -short_name: gke -environment: prod -folder_config: - name: Production - parent_id: gke -stage2_iam: - networking: - sa_roles: - ro: - - roles/dns.reader - rw: - - roles/dns.admin diff --git a/fast/stages/1-resman/data/stage-3/project-factory-prod.yaml b/fast/stages/1-resman/data/stage-3/project-factory-prod.yaml deleted file mode 100644 index bc6b3eb7e..000000000 --- a/fast/stages/1-resman/data/stage-3/project-factory-prod.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2024 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/fast-stage3.schema.json - -short_name: pf -environment: prod -stage2_iam: - networking: - iam_admin_delegated: true - security: - iam_admin_delegated: true diff --git a/fast/stages/1-resman/data/top-level-folders/sandbox.yaml b/fast/stages/1-resman/data/top-level-folders/sandbox.yaml index e43a59dfe..1ce256fcf 100644 --- a/fast/stages/1-resman/data/top-level-folders/sandbox.yaml +++ b/fast/stages/1-resman/data/top-level-folders/sandbox.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# 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. @@ -20,8 +20,8 @@ automation: short_name: sbox # You can create role bindings referring to the automation service account by # referring to it using `self` keyword, per the example below -iam: - "roles/owner": +iam: + roles/owner: - self factories_config: org_policies: data/org-policies/sandbox diff --git a/fast/stages/1-resman/data/top-level-folders/teams.yaml b/fast/stages/1-resman/data/top-level-folders/teams.yaml index caa1112c4..907bdd7b2 100644 --- a/fast/stages/1-resman/data/top-level-folders/teams.yaml +++ b/fast/stages/1-resman/data/top-level-folders/teams.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# 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. @@ -15,23 +15,18 @@ # yaml-language-server: $schema=../../schemas/top-level-folder.schema.json name: Teams -iam: - "roles/owner": - - project-factory - "roles/resourcemanager.folderAdmin": - - project-factory - "roles/resourcemanager.projectCreator": - - project-factory - "roles/resourcemanager.tagUser": - - project-factory - "service_project_network_admin": - - project-factory - "roles/viewer": - - project-factory-r - "roles/resourcemanager.folderViewer": - - project-factory-r - "roles/resourcemanager.tagViewer": - - project-factory-r +iam_by_principals: + project-factory-rw: + - roles/owner + - roles/resourcemanager.folderAdmin + - roles/resourcemanager.projectCreator + - roles/resourcemanager.tagUser + - service_project_network_admin + project-factory-ro: + - roles/viewer + - roles/resourcemanager.folderViewer + - roles/resourcemanager.tagViewer + # don't create a context tag since this uses the pf tag is_fast_context: false tag_bindings: diff --git a/fast/stages/1-resman/iam.tf b/fast/stages/1-resman/iam.tf deleted file mode 100644 index 3116fd19f..000000000 --- a/fast/stages/1-resman/iam.tf +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright 2024 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. - */ - -# tfdoc:file:description Organization or root node-level IAM bindings. - -locals { - # aggregated map of organization IAM additive bindings for stages - iam_bindings_additive = merge( - # stage 2 networking - !var.fast_stage_2.networking.enabled ? {} : { - sa_net_rw_fw_policy_admin = { - member = module.net-sa-rw[0].iam_email - role = "roles/compute.orgFirewallPolicyAdmin" - } - sa_net_rw_ngfw_enterprise_admin = { - member = module.net-sa-rw[0].iam_email - role = var.custom_roles["ngfw_enterprise_admin"], - } - sa_net_rw_xpn_admin = { - member = module.net-sa-rw[0].iam_email - role = "roles/compute.xpnAdmin" - } - sa_net_ro_fw_policy_user = { - member = module.net-sa-ro[0].iam_email - role = "roles/compute.orgFirewallPolicyUser" - } - sa_net_net_ro_ngfw_enterprise_viewer = { - member = module.net-sa-ro[0].iam_email - role = var.custom_roles["ngfw_enterprise_viewer"], - } - }, - # stage 2 security - !var.fast_stage_2.security.enabled ? {} : { - sa_sec_asset_viewer = { - member = module.sec-sa-rw[0].iam_email - role = "roles/cloudasset.viewer" - } - }, - # stage 2 project factory - var.root_node != null || var.fast_stage_2.project_factory.enabled != true ? {} : { - sa_pf_conditional_org_policy = { - member = module.pf-sa-rw[0].iam_email - role = "roles/orgpolicy.policyAdmin" - condition = { - title = "org_policy_tag_pf_scoped" - description = "Org policy tag scoped grant for project factory." - expression = <<-END - resource.matchTag('${local.tag_root}/${var.tag_names.context}', 'project-factory') - END - } - } - }, - # stage 3 - { - for v in local.stage3_sa_roles_in_org : join("/", values(v)) => { - role = lookup(var.custom_roles, v.role, v.role) - member = ( - v.sa == "rw" - ? module.stage3-sa-rw[v.s3].iam_email - : module.stage3-sa-ro[v.s3].iam_email - ) - condition = { - title = "stage3 ${v.s3} ${v.env}" - expression = <<-END - resource.matchTag( - '${local.tag_root}/${var.tag_names.environment}', - '${v.env}' - ) - && - resource.matchTag( - '${local.tag_root}/${var.tag_names.context}', - '${v.context}' - ) - END - } - } - }, - # billing for all stages - local.billing_mode != "org" ? {} : local.billing_iam - ) -} diff --git a/fast/stages/1-resman/main.tf b/fast/stages/1-resman/main.tf index fee31c65a..3674087df 100644 --- a/fast/stages/1-resman/main.tf +++ b/fast/stages/1-resman/main.tf @@ -15,6 +15,9 @@ */ locals { + environment_default = [ + for k, v in var.environments : v if v.is_default + ][0] identity_providers = coalesce( try(var.automation.federated_identity_providers, null), {} ) @@ -25,6 +28,10 @@ locals { : "group:${v}@${var.organization.domain}" ) } + principals_iam = merge(local.principals, { + for k, v in local.stage_service_accounts : + replace(k, "_", "-") => "serviceAccount:${v}" + }) root_node = ( var.root_node == null ? "organizations/${var.organization.id}" @@ -39,20 +46,10 @@ locals { } # combined list of stage service accounts stage_service_accounts = merge( - !var.fast_stage_2.networking.enabled ? {} : { - networking = module.net-sa-rw[0].email - networking-r = module.net-sa-ro[0].email - }, - !var.fast_stage_2.security.enabled ? {} : { - security = module.sec-sa-rw[0].email - security-r = module.sec-sa-ro[0].email - }, - !var.fast_stage_2.project_factory.enabled ? {} : { - project-factory = module.pf-sa-rw[0].email - project-factory-r = module.pf-sa-ro[0].email - }, - { for k, v in local.stage3 : k => module.stage3-sa-rw[k].email }, - { for k, v in local.stage3 : "${k}-r" => module.stage3-sa-ro[k].email }, + { for k, v in local.stage2 : "${k}-rw" => module.stage2-sa-rw[k].email }, + { for k, v in local.stage2 : "${k}-ro" => module.stage2-sa-ro[k].email }, + { for k, v in local.stage3 : "${k}-rw" => module.stage3-sa-rw[k].email }, + { for k, v in local.stage3 : "${k}-ro" => module.stage3-sa-ro[k].email }, ) tag_keys = ( var.root_node == null diff --git a/fast/stages/1-resman/moved/v37.0.0.tf b/fast/stages/1-resman/moved/v37.0.0.tf new file mode 100644 index 000000000..ae99dcf0a --- /dev/null +++ b/fast/stages/1-resman/moved/v37.0.0.tf @@ -0,0 +1,80 @@ +/** + * 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. + */ + +# networking +moved { + from = module.net-folder[0] + to = module.stage2-folder["networking"] +} +moved { + from = module.net-folder-envs["dev"] + to = module.stage2-folder-env["networking-dev"] +} +moved { + from = module.net-folder-envs["prod"] + to = module.stage2-folder-env["networking-prod"] +} +moved { + from = module.net-bucket[0] + to = module.stage2-bucket["networking"] +} +moved { + from = module.net-sa-ro[0] + to = module.stage2-sa-ro["networking"] +} +moved { + from = module.net-sa-rw[0] + to = module.stage2-sa-rw["networking"] +} +# project factory +# resources change prefix and are recreated anyway +moved { + from = module.pf-bucket[0] + to = module.stage2-bucket["project-factory"] +} +moved { + from = module.pf-sa-ro[0] + to = module.stage2-sa-ro["project-factory"] +} +moved { + from = module.pf-sa-rw[0] + to = module.stage2-sa-rw["project-factory"] +} +# security +moved { + from = module.sec-folder[0] + to = module.stage2-folder["security"] +} +moved { + from = module.sec-folder-envs["dev"] + to = module.stage2-folder-env["security-dev"] +} +moved { + from = module.sec-folder-envs["prod"] + to = module.stage2-folder-env["security-prod"] +} +moved { + from = module.sec-bucket[0] + to = module.stage2-bucket["security"] +} +moved { + from = module.sec-sa-ro[0] + to = module.stage2-sa-ro["security"] +} +moved { + from = module.sec-sa-rw[0] + to = module.stage2-sa-rw["security"] +} diff --git a/fast/stages/1-resman/organization.tf b/fast/stages/1-resman/organization.tf index 4c327d234..3927e13d3 100644 --- a/fast/stages/1-resman/organization.tf +++ b/fast/stages/1-resman/organization.tf @@ -19,8 +19,7 @@ locals { # context tag values for enabled stage 2s (merged in the final map below) _context_tag_values_stage2 = { - for k, v in var.fast_stage_2 : - k => replace(k, "_", "-") if v.enabled + for k, v in local.stage2 : k => replace(k, "_", "-") } # merge all context tag values into a single map context_tag_values = merge( @@ -52,15 +51,11 @@ locals { { "roles/resourcemanager.tagUser" = distinct(concat( try(local.tags.environment.values[v.tag_name].iam["roles/resourcemanager.tagUser"], []), - !var.fast_stage_2.project_factory.enabled ? [] : [module.pf-sa-rw[0].iam_email], - !var.fast_stage_2.networking.enabled ? [] : [module.net-sa-rw[0].iam_email], - !var.fast_stage_2.security.enabled ? [] : [module.sec-sa-rw[0].iam_email], + [for k, v in module.stage2-sa-rw : v.iam_email] )) "roles/resourcemanager.tagViewer" = distinct(concat( try(local.tags.environment.values[v.tag_name].iam["roles/resourcemanager.tagViewer"], []), - !var.fast_stage_2.project_factory.enabled ? [] : [module.pf-sa-ro[0].iam_email], - !var.fast_stage_2.networking.enabled ? [] : [module.net-sa-ro[0].iam_email], - !var.fast_stage_2.security.enabled ? [] : [module.sec-sa-ro[0].iam_email], + [for k, v in module.stage2-sa-ro : v.iam_email] )) } ) @@ -69,27 +64,27 @@ locals { ) } } - # service account expansion for user-specified tag values + # combine org-level IAM additive from billing and stage 2s + iam_bindings_additive = merge( + merge([ + for k, v in local.stage2 : + v.organization_config.iam_bindings_additive + ]...), + local.billing_mode != "org" ? {} : local.billing_iam + ) + # IAM principal expansion for user-specified tag values tags = { for k, v in var.tags : k => merge(v, { iam = { for rk, rv in v.iam : rk => [ - for rm in rv : ( - contains(keys(local.service_accounts), rm) - ? "serviceAccount:${local.service_accounts[rm]}" - : rm - ) + for rm in rv : lookup(local.principals_iam, rm, rm) ] } values = { for vk, vv in v.values : vk => merge(vv, { iam = { for rk, rv in vv.iam : rk => [ - for rm in rv : ( - contains(keys(local.service_accounts), rm) - ? "serviceAccount:${local.service_accounts[rm]}" - : rm - ) + for rm in rv : lookup(local.principals_iam, rm, rm) ] } }) @@ -103,7 +98,13 @@ module "organization" { count = var.root_node == null ? 1 : 0 organization_id = "organizations/${var.organization.id}" # additive bindings leveraging the delegated IAM grant set in stage 0 - iam_bindings_additive = local.iam_bindings_additive + iam_bindings_additive = { + for k, v in local.iam_bindings_additive : k => { + role = lookup(var.custom_roles, v.role, v.role) + member = lookup(local.principals_iam, v.member, v.member) + condition = lookup(v, "condition", null) + } + } # do not assign tagViewer or tagUser roles here on tag keys and values as # they are managed authoritatively and will break multitenant stages tags = merge(local.tags, { diff --git a/fast/stages/1-resman/outputs-cicd.tf b/fast/stages/1-resman/outputs-cicd.tf index 1e0c2f0d2..ac49f7455 100644 --- a/fast/stages/1-resman/outputs-cicd.tf +++ b/fast/stages/1-resman/outputs-cicd.tf @@ -18,31 +18,6 @@ locals { # render CI/CD workflow templates - - _stage_2_cicd_workflow_var_files = { - security = [ - "0-bootstrap.auto.tfvars.json", - "1-resman.auto.tfvars.json", - "0-globals.auto.tfvars.json" - ] - networking = [ - "0-bootstrap.auto.tfvars.json", - "1-resman.auto.tfvars.json", - "0-globals.auto.tfvars.json" - ] - network_security = [ - "0-bootstrap.auto.tfvars.json", - "1-resman.auto.tfvars.json", - "0-globals.auto.tfvars.json" - ] - project_factory = [ - "0-bootstrap.auto.tfvars.json", - "1-resman.auto.tfvars.json", - "0-globals.auto.tfvars.json", - "2-networking.auto.tfvars.json" - ] - } - cicd_workflows = { for k, v in local.cicd_repositories : "${v.level}-${replace(k, "_", "-")}" => templatefile( "${path.module}/templates/workflow-${v.repository.type}.yaml", { @@ -66,7 +41,11 @@ locals { } tf_var_files = ( v.level == 2 ? - local._stage_2_cicd_workflow_var_files[k] + [ + "0-bootstrap.auto.tfvars.json", + "1-resman.auto.tfvars.json", + "0-globals.auto.tfvars.json" + ] : [ "0-bootstrap.auto.tfvars.json", "0-globals.auto.tfvars.json", diff --git a/fast/stages/1-resman/outputs-providers.tf b/fast/stages/1-resman/outputs-providers.tf index 8ed7b6c58..87636e80e 100644 --- a/fast/stages/1-resman/outputs-providers.tf +++ b/fast/stages/1-resman/outputs-providers.tf @@ -17,89 +17,46 @@ # tfdoc:file:description Locals for provider output files. locals { - # output file definitions for enabled stage 2s - _stage2_outputs_attrs = merge( - var.fast_stage_2["networking"].enabled != true ? {} : { - networking = { - bucket = module.net-bucket[0].name - sa = { - apply = module.net-sa-rw[0].email - plan = module.net-sa-ro[0].email - } - } - }, - var.fast_stage_2["project_factory"].enabled != true ? {} : { - project_factory = { - bucket = module.pf-bucket[0].name - sa = { - apply = module.pf-sa-rw[0].email - plan = module.pf-sa-ro[0].email - } - } - }, - var.fast_stage_2["security"].enabled != true ? {} : { - security = { - bucket = module.sec-bucket[0].name - sa = { - apply = module.sec-sa-rw[0].email - plan = module.sec-sa-ro[0].email - } - } - }, - # TODO: use ${parent_stage}-${key} for the addon file output names - # addons, conditions are repeated to prevent inconsistent result type error - var.fast_stage_2["networking"].enabled != true ? {} : { - for k, v in local.stage_addons : "networking-${k}" => { - bucket = module.net-bucket[0].name - backend_extra = "prefix = \"addons/${k}\"" - sa = { - apply = module.net-sa-rw[0].email - plan = module.net-sa-ro[0].email - } - } if v.parent_stage == "2-networking" - }, - var.fast_stage_2["project_factory"].enabled != true ? {} : { - for k, v in local.stage_addons : "pf-${k}" => { - bucket = module.pf-bucket[0].name - backend_extra = "prefix = \"addons/${k}\"" - sa = { - apply = module.pf-sa-rw[0].email - plan = module.pf-sa-ro[0].email - } - } if v.parent_stage == "2-project-factory" - }, - var.fast_stage_2["security"].enabled != true ? {} : { - for k, v in local.stage_addons : "security-${k}" => { - bucket = module.sec-bucket[0].name - backend_extra = "prefix = \"addons/${k}\"" - sa = { - apply = module.sec-sa-rw[0].email - plan = module.sec-sa-ro[0].email - } - } if v.parent_stage == "2-security" - } - ) # render provider files from template providers = merge( # stage 2 { - for k, v in local._stage2_outputs_attrs : - "2-${replace(k, "_", "-")}" => templatefile(local._tpl_providers, { - backend_extra = lookup(v, "backend_extra", null) - bucket = v.bucket + for k, v in local.stage2 : + "2-${k}" => templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.stage2-bucket[k].name name = k - sa = v.sa.apply + sa = module.stage2-sa-rw[k].email }) }, { - for k, v in local._stage2_outputs_attrs : - "2-${replace(k, "_", "-")}-r" => templatefile(local._tpl_providers, { - backend_extra = lookup(v, "backend_extra", null) - bucket = v.bucket + for k, v in local.stage2 : + "2-${k}-r" => templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.stage2-bucket[k].name name = k - sa = v.sa.plan + sa = module.stage2-sa-ro[k].email }) }, + # stage 2 addons + { + for k, v in local.stage_addons : + "${v.parent_stage}-${v.short_name}" => templatefile(local._tpl_providers, { + backend_extra = "prefix = \"addons/${k}\"" + bucket = module.stage2-bucket[v.stage.name].name + name = "${v.stage.name}-${v.short_name}" + sa = module.stage2-sa-rw[v.stage.name].email + }) if lookup(local.stage2, v.stage.name, null) != null + }, + { + for k, v in local.stage_addons : + "${v.parent_stage}-${v.short_name}-r" => templatefile(local._tpl_providers, { + backend_extra = "prefix = \"addons/${k}\"" + bucket = module.stage2-bucket[v.stage.name].name + name = "${v.stage.name}-${v.short_name}" + sa = module.stage2-sa-ro[v.stage.name].email + }) if lookup(local.stage2, v.stage.name, null) != null + }, # stage 3 { for k, v in local.stage3 : diff --git a/fast/stages/1-resman/outputs.tf b/fast/stages/1-resman/outputs.tf index e7a8ac19d..b55c4f6f7 100644 --- a/fast/stages/1-resman/outputs.tf +++ b/fast/stages/1-resman/outputs.tf @@ -17,14 +17,8 @@ locals { folder_ids = merge( # stage 2 - !var.fast_stage_2.networking.enabled ? {} : merge( - { networking = module.net-folder[0].id }, - { for k, v in module.net-folder-envs : "networking-${k}" => v.id } - ), - !var.fast_stage_2.security.enabled ? {} : merge( - { security = module.sec-folder[0].id }, - { for k, v in module.sec-folder-envs : "security-${k}" => v.id } - ), + { for k, v in module.stage2-folder : k => v.id }, + { for k, v in module.stage2-folder-env : k => v.id }, # stage 3 { for k, v in module.stage3-folder : k => v.id }, # top-level folders @@ -43,24 +37,21 @@ locals { } }, { - for k, v in var.fast_stage_2 : k => { + for k, v in local.stage2 : k => { short_name = v.short_name - # rw service accounts for stage 3s that need delegated IAM on stage 2s - iam_delegated_principals = { - for ek, _ in var.environments : ek => [ - for sk, sv in local.stage3 : - "serviceAccount:${local.stage_service_accounts[sk]}" - if sv.environment == ek && try(sv.stage2_iam[k].iam_admin_delegated, false) - ] + iam_admin_delegated = { + for kk in v.stage3_config.iam_admin_delegated : + kk.environment => lookup( + local.principals_iam, kk.principal, kk.principal + )... } - iam_viewer_principals = { - for ek, _ in var.environments : ek => [ - for sk, sv in local.stage3 : - "serviceAccount:${local.stage_service_accounts["${sk}-r"]}" - if sv.environment == ek && try(sv.stage2_iam[k].iam_admin_delegated, false) - ] + iam_viewer = { + for kk in v.stage3_config.iam_viewer : + kk.environment => lookup( + local.principals_iam, kk.principal, kk.principal + )... } - } if v.enabled == true + } } ) folder_ids = local.folder_ids @@ -95,6 +86,11 @@ output "providers" { value = local.providers } +output "service_accounts" { + description = "Service accounts." + value = local.service_accounts +} + # ready to use variable values for subsequent stages output "tfvars" { description = "Terraform variable files for the following stages." diff --git a/fast/stages/1-resman/schemas/fast-stage2.schema.json b/fast/stages/1-resman/schemas/fast-stage2.schema.json new file mode 100644 index 000000000..bb2c45faa --- /dev/null +++ b/fast/stages/1-resman/schemas/fast-stage2.schema.json @@ -0,0 +1,310 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FAST stage 2", + "type": "object", + "additionalProperties": false, + "properties": { + "short_name": { + "type": "string" + }, + "cicd_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "identity_provider", + "repository" + ], + "properties": { + "identity_provider": { + "type": "string" + }, + "repository": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "github", + "gitlab" + ], + "default": "github" + } + } + } + } + }, + "folder_config": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "create_env_folders": { + "type": "boolean", + "default": true + }, + "iam": { + "$ref": "#/$defs/iam" + }, + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_by_principals": { + "$ref": "#/$defs/iam_by_principals" + }, + "org_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z]+\\.": { + "inherit_from_parent": { + "type": "boolean" + }, + "reset": { + "type": "boolean" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "allow": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "deny": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "enforce": { + "type": "boolean" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "expression": { + "type": "string" + }, + "location": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "parent_id": { + "type": "string" + } + } + }, + "organization_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_by_principals": { + "$ref": "#/$defs/iam_by_principals" + } + } + }, + "stage3_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "iam_admin_delegated": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "environment": { + "type": "string" + }, + "principal": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" + } + } + } + }, + "iam_viewer": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "environment": { + "type": "string" + }, + "principal": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" + } + } + } + } + } + } + }, + "$defs": { + "iam": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^(?:roles/|[a-z_]+)": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" + } + } + } + }, + "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:|[a-z])" + } + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|[a-z])" + }, + "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:|[a-z])" + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|[a-z])" + }, + "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": { + "^[a-z]+[a-z-]+$": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:roles/|[a-z_]+)" + } + } + } + } + } +} \ No newline at end of file diff --git a/fast/stages/1-resman/schemas/fast-stage3.schema.json b/fast/stages/1-resman/schemas/fast-stage3.schema.json index 80bbad749..cacf854be 100644 --- a/fast/stages/1-resman/schemas/fast-stage3.schema.json +++ b/fast/stages/1-resman/schemas/fast-stage3.schema.json @@ -62,19 +62,6 @@ "name": { "type": "string" }, - "iam_by_principals": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:)": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?:roles/|[a-z_]+)" - } - } - } - }, "parent_id": { "type": "string" }, @@ -86,65 +73,201 @@ "type": "string" } } - } - } - }, - "organization_iam": { - "type": "object", - "additionalProperties": false, - "required": [ - "context_tag_value" - ], - "properties": { - "context_tag_value": { - "type": "string" }, - "sa_roles": { - "$ref": "#/$defs/sa_roles" - } - } - }, - "stage2_iam": { - "type": "object", - "additionalProperties": false, - "properties": { - "networking": { - "$ref": "#/$defs/stage2_iam" + "iam": { + "$ref": "#/$defs/iam" }, - "security": { - "$ref": "#/$defs/stage2_iam" + "iam_bindings": { + "$ref": "#/$defs/iam_bindings" + }, + "iam_bindings_additive": { + "$ref": "#/$defs/iam_bindings_additive" + }, + "iam_by_principals": { + "$ref": "#/$defs/iam_by_principals" + }, + "org_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z]+\\.": { + "inherit_from_parent": { + "type": "boolean" + }, + "reset": { + "type": "boolean" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "allow": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "deny": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "enforce": { + "type": "boolean" + }, + "condition": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "expression": { + "type": "string" + }, + "location": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + } + } + } + } } } } }, "$defs": { - "sa_roles": { + "iam": { "type": "object", "additionalProperties": false, - "properties": { - "ro": { + "patternProperties": { + "^(?:roles/|[a-z_]+)": { "type": "array", "items": { - "type": "string" - } - }, - "rw": { - "type": "array", - "items": { - "type": "string" + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" } } } }, - "stage2_iam": { + "iam_bindings": { "type": "object", "additionalProperties": false, - "properties": { - "iam_admin_delegated": { - "type": "boolean" - }, - "sa_roles": { - "$ref": "#/$defs/sa_roles" + "patternProperties": { + "^[a-z0-9_-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "members": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|[a-z])" + } + }, + "role": { + "type": "string", + "pattern": "^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:|[a-z])" + }, + "role": { + "type": "string", + "pattern": "^(?:roles/|[a-z])" + }, + "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": { + "^[a-z]+[a-z-]+$": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?:roles/|[a-z_]+)" + } } } } diff --git a/fast/stages/1-resman/schemas/top-level-folder.schema.json b/fast/stages/1-resman/schemas/top-level-folder.schema.json index 6329fbb8f..a0cbc957d 100644 --- a/fast/stages/1-resman/schemas/top-level-folder.schema.json +++ b/fast/stages/1-resman/schemas/top-level-folder.schema.json @@ -351,7 +351,7 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:)": { + "^[a-z]+[a-z-]+$": { "type": "array", "items": { "type": "string", diff --git a/fast/stages/1-resman/stage-2-networking.tf b/fast/stages/1-resman/stage-2-networking.tf deleted file mode 100644 index a61dae41a..000000000 --- a/fast/stages/1-resman/stage-2-networking.tf +++ /dev/null @@ -1,209 +0,0 @@ -/** - * Copyright 2024 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. - */ - -locals { - # filter and normalize stage 3 roles applied to this stage's top-level folder - net_s3_iam = !var.fast_stage_2.networking.enabled ? {} : { - for v in local.stage3_iam_in_stage2 : "${v.role}:${v.env}" => ( - v.sa == "rw" - ? module.stage3-sa-rw[v.s3].iam_email - : module.stage3-sa-ro[v.s3].iam_email - )... - if v.s2 == "networking" - } - net_use_env_folders = ( - var.fast_stage_2.networking.enabled && - var.fast_stage_2.networking.folder_config.create_env_folders - ) -} - -# top-level folder - -module "net-folder" { - source = "../../../modules/folder" - count = var.fast_stage_2.networking.enabled ? 1 : 0 - parent = ( - var.fast_stage_2.networking.folder_config.parent_id == null - ? local.root_node - : try( - local.top_level_folder_ids[var.fast_stage_2.networking.folder_config.parent_id], - var.fast_stage_2.networking.folder_config.parent_id - ) - ) - name = var.fast_stage_2.networking.folder_config.name - iam = merge( - # stage own service accounts - { - "roles/logging.admin" = [module.net-sa-rw[0].iam_email] - "roles/owner" = [module.net-sa-rw[0].iam_email] - "roles/resourcemanager.folderAdmin" = [module.net-sa-rw[0].iam_email] - "roles/resourcemanager.projectCreator" = [module.net-sa-rw[0].iam_email] - "roles/compute.xpnAdmin" = [module.net-sa-rw[0].iam_email] - "roles/resourcemanager.tagUser" = [module.net-sa-rw[0].iam_email] - "roles/viewer" = [module.net-sa-ro[0].iam_email] - "roles/resourcemanager.folderViewer" = [module.net-sa-ro[0].iam_email] - "roles/resourcemanager.tagViewer" = [module.net-sa-ro[0].iam_email] - }, - # security stage 2 service accounts - var.fast_stage_2.security.enabled != true ? {} : { - "roles/serviceusage.serviceUsageAdmin" = [ - module.sec-sa-rw[0].iam_email - ] - "roles/serviceusage.serviceUsageConsumer" = [ - module.sec-sa-ro[0].iam_email - ] - }, - # project factory service accounts - (var.fast_stage_2.project_factory.enabled) != true ? {} : { - (var.custom_roles.service_project_network_admin) = [ - module.pf-sa-rw[0].iam_email - ] - (var.custom_roles.project_iam_viewer) = [ - module.pf-sa-ro[0].iam_email - ] - "roles/compute.networkViewer" = [ - module.pf-sa-ro[0].iam_email - ] - } - ) - iam_bindings = merge( - # project factory delegated grant - var.fast_stage_2.project_factory.enabled != true ? {} : { - pf_delegated_grant = { - role = "roles/resourcemanager.projectIamAdmin" - members = [module.pf-sa-rw[0].iam_email] - condition = { - expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", - "'roles/compute.networkUser', 'roles/composer.sharedVpcAgent', 'roles/container.hostServiceAgentUser', 'roles/vpcaccess.user'" - ) - title = "project factory project delegated admin" - description = "Project factory delegated grant." - } - } - }, - # stage 3 roles - { - for k, v in local.net_s3_iam : k => { - role = lookup(var.custom_roles, split(":", k)[0], split(":", k)[0]) - members = v - condition = { - title = "stage 3 ${split(":", k)[1]}" - expression = <<-END - resource.matchTag( - '${local.tag_root}/${var.tag_names.environment}', - '${split(":", k)[1]}' - ) - END - } - } - } - ) - iam_by_principals = merge( - { - # replace with more selective custom roles for production deployments - (local.principals.gcp-network-admins) = ["roles/editor"] - }, - var.fast_stage_2.networking.folder_config.iam_by_principals - ) - tag_bindings = { - context = try( - local.tag_values["${var.tag_names.context}/networking"].id, null - ) - } -} - -# optional per-environment folders - -module "net-folder-envs" { - source = "../../../modules/folder" - for_each = local.net_use_env_folders ? var.environments : {} - parent = module.net-folder[0].id - name = each.value.name - tag_bindings = { - environment = try( - local.tag_values["${var.tag_names.environment}/${each.value.tag_name}"].id, - null - ) - } -} - -# automation service accounts - -module "net-sa-rw" { - source = "../../../modules/iam-service-account" - count = var.fast_stage_2.networking.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["sa-net_rw"], { - name = var.fast_stage_2.networking.short_name - }) - display_name = "Terraform resman networking service account." - prefix = var.prefix - service_account_create = var.root_node == null - iam = { - "roles/iam.serviceAccountTokenCreator" = [ - for k, v in local.cicd_repositories : - module.cicd-sa-rw[k].iam_email if v.stage == "networking" - ] - } - iam_project_roles = { - (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] - } - iam_storage_roles = { - (var.automation.outputs_bucket) = ["roles/storage.objectAdmin"] - } -} - -module "net-sa-ro" { - source = "../../../modules/iam-service-account" - count = var.fast_stage_2.networking.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["sa-net_ro"], { - name = var.fast_stage_2.networking.short_name - }) - display_name = "Terraform resman networking service account (read-only)." - prefix = var.prefix - iam = { - "roles/iam.serviceAccountTokenCreator" = [ - for k, v in local.cicd_repositories : - module.cicd-sa-ro[k].iam_email if v.stage == "networking" - ] - } - iam_project_roles = { - (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] - } - iam_storage_roles = { - (var.automation.outputs_bucket) = [var.custom_roles["storage_viewer"]] - } -} - -# automation bucket - -module "net-bucket" { - source = "../../../modules/gcs" - count = var.fast_stage_2.networking.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["gcs-net"], { - name = var.fast_stage_2.networking.short_name - }) - prefix = var.prefix - location = var.locations.gcs - versioning = true - iam = { - "roles/storage.objectAdmin" = [module.net-sa-rw[0].iam_email] - "roles/storage.objectViewer" = [module.net-sa-ro[0].iam_email] - } -} diff --git a/fast/stages/1-resman/stage-2-project-factory.tf b/fast/stages/1-resman/stage-2-project-factory.tf deleted file mode 100644 index 91500bdca..000000000 --- a/fast/stages/1-resman/stage-2-project-factory.tf +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright 2024 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. - */ - -# automation service accounts - -module "pf-sa-rw" { - source = "../../../modules/iam-service-account" - count = var.fast_stage_2.project_factory.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["sa-pf_rw"], { - name = var.fast_stage_2.project_factory.short_name - }) - display_name = "Terraform resman project factory main service account." - prefix = var.prefix - iam = { - "roles/iam.serviceAccountTokenCreator" = [ - for k, v in local.cicd_repositories : - module.cicd-sa-rw[k].iam_email if v.stage == "project-factory" - ] - } - iam_project_roles = { - (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] - } - iam_storage_roles = { - (var.automation.outputs_bucket) = ["roles/storage.objectAdmin"] - } -} - -module "pf-sa-ro" { - source = "../../../modules/iam-service-account" - count = var.fast_stage_2.project_factory.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["sa-pf_ro"], { - name = var.fast_stage_2.project_factory.short_name - }) - display_name = "Terraform resman project factory main service account (read-only)." - prefix = var.prefix - iam = { - "roles/iam.serviceAccountTokenCreator" = [ - for k, v in local.cicd_repositories : - module.cicd-sa-ro[k].iam_email if v.stage == "project-factory" - ] - } - iam_project_roles = { - (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] - } - iam_storage_roles = { - (var.automation.outputs_bucket) = [var.custom_roles["storage_viewer"]] - } -} - -# automation bucket - -module "pf-bucket" { - source = "../../../modules/gcs" - count = var.fast_stage_2.project_factory.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["gcs-pf"], { - name = var.fast_stage_2.project_factory.short_name - }) - prefix = var.prefix - location = var.locations.gcs - versioning = true - iam = { - "roles/storage.objectAdmin" = [module.pf-sa-rw[0].iam_email] - "roles/storage.objectViewer" = [module.pf-sa-ro[0].iam_email] - } -} diff --git a/fast/stages/1-resman/stage-2-security.tf b/fast/stages/1-resman/stage-2-security.tf deleted file mode 100644 index 20028ed4f..000000000 --- a/fast/stages/1-resman/stage-2-security.tf +++ /dev/null @@ -1,201 +0,0 @@ -/** - * 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. - */ - -locals { - # filter and normalize stage 3 roles applied to this stage's top-level folder - sec_s3_iam = !var.fast_stage_2.security.enabled ? {} : { - for v in local.stage3_iam_in_stage2 : "${v.role}:${v.env}" => ( - v.sa == "rw" - ? module.stage3-sa-rw[v.s3].iam_email - : module.stage3-sa-ro[v.s3].iam_email - )... - if v.s2 == "security" - } - sec_use_env_folders = ( - var.fast_stage_2.security.enabled && - var.fast_stage_2.security.folder_config.create_env_folders - ) -} - -# top-level folder - -module "sec-folder" { - source = "../../../modules/folder" - count = var.fast_stage_2.security.enabled ? 1 : 0 - parent = ( - var.fast_stage_2.security.folder_config.parent_id == null - ? local.root_node - : try( - local.top_level_folder_ids[var.fast_stage_2.security.folder_config.parent_id], - var.fast_stage_2.security.folder_config.parent_id - ) - ) - name = var.fast_stage_2.security.folder_config.name - iam = merge( - # stage own service accounts - { - "roles/logging.admin" = [module.sec-sa-rw[0].iam_email] - "roles/owner" = [module.sec-sa-rw[0].iam_email] - "roles/resourcemanager.folderAdmin" = [module.sec-sa-rw[0].iam_email] - "roles/resourcemanager.projectCreator" = [module.sec-sa-rw[0].iam_email] - "roles/viewer" = [module.sec-sa-ro[0].iam_email] - "roles/resourcemanager.folderViewer" = [module.sec-sa-ro[0].iam_email] - }, - # networking service accounts - (var.fast_stage_2.networking.enabled) != true ? {} : { - "roles/resourcemanager.tagUser" = [module.net-sa-rw[0].iam_email] - "roles/resourcemanager.tagViewer" = [module.net-sa-ro[0].iam_email] - }, - # project factory service accounts - (var.fast_stage_2.project_factory.enabled) != true ? {} : { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - module.pf-sa-rw[0].iam_email - ] - (var.custom_roles.project_iam_viewer) = [ - module.pf-sa-ro[0].iam_email - ] - "roles/cloudkms.viewer" = [ - module.pf-sa-ro[0].iam_email - ] - } - ) - iam_bindings = merge( - var.fast_stage_2.project_factory.enabled != true ? {} : { - pf_delegated_grant = { - role = "roles/resourcemanager.projectIamAdmin" - members = [module.pf-sa-rw[0].iam_email] - condition = { - expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", - "'roles/cloudkms.cryptoKeyEncrypterDecrypter'" - ) - title = "pf_delegated_grant" - description = "Project factory delegated grant." - } - } - }, - # stage 3 IAM bindings use conditions based on environment - { - for k, v in local.sec_s3_iam : k => { - role = lookup(var.custom_roles, split(":", k)[0], split(":", k)[0]) - members = v - condition = { - title = "stage 3 ${split(":", k)[1]}" - expression = <<-END - resource.matchTag( - '${local.tag_root}/${var.tag_names.environment}', - '${split(":", k)[1]}' - ) - END - } - } - } - ) - iam_by_principals = merge( - { - # replace with more selective custom roles for production deployments - (local.principals.gcp-security-admins) = ["roles/editor"] - }, - var.fast_stage_2.security.folder_config.iam_by_principals - ) - tag_bindings = { - context = try( - local.tag_values["${var.tag_names.context}/security"].id, null - ) - } -} - -# optional per-environment folders - -module "sec-folder-envs" { - source = "../../../modules/folder" - for_each = local.sec_use_env_folders ? var.environments : {} - parent = module.sec-folder[0].id - name = each.value.name - tag_bindings = { - environment = try( - local.tag_values["${var.tag_names.environment}/${each.value.tag_name}"].id, - null - ) - } -} - -# automation service accounts - -module "sec-sa-rw" { - source = "../../../modules/iam-service-account" - count = var.fast_stage_2.security.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["sa-sec_rw"], { - name = var.fast_stage_2.security.short_name - }) - display_name = "Terraform resman security service account." - prefix = var.prefix - service_account_create = var.root_node == null - iam = { - "roles/iam.serviceAccountTokenCreator" = [ - for k, v in local.cicd_repositories : - module.cicd-sa-rw[k].iam_email if v.stage == "security" - ] - } - iam_project_roles = { - (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] - } - iam_storage_roles = { - (var.automation.outputs_bucket) = ["roles/storage.objectAdmin"] - } -} - -module "sec-sa-ro" { - source = "../../../modules/iam-service-account" - count = var.fast_stage_2.security.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["sa-sec_ro"], { - name = var.fast_stage_2.security.short_name - }) - display_name = "Terraform resman security service account (read-only)." - prefix = var.prefix - iam = { - "roles/iam.serviceAccountTokenCreator" = [ - for k, v in local.cicd_repositories : - module.cicd-sa-ro[k].iam_email if v.stage == "security" - ] - } - iam_project_roles = { - (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] - } - iam_storage_roles = { - (var.automation.outputs_bucket) = [var.custom_roles["storage_viewer"]] - } -} - -# automation bucket - -module "sec-bucket" { - source = "../../../modules/gcs" - count = var.fast_stage_2.security.enabled ? 1 : 0 - project_id = var.automation.project_id - name = templatestring(var.resource_names["gcs-sec"], { - name = var.fast_stage_2.security.short_name - }) - prefix = var.prefix - location = var.locations.gcs - versioning = true - iam = { - "roles/storage.objectAdmin" = [module.sec-sa-rw[0].iam_email] - "roles/storage.objectViewer" = [module.sec-sa-ro[0].iam_email] - } -} diff --git a/fast/stages/1-resman/stage-2.tf b/fast/stages/1-resman/stage-2.tf new file mode 100644 index 000000000..f512d35db --- /dev/null +++ b/fast/stages/1-resman/stage-2.tf @@ -0,0 +1,284 @@ +/** + * 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. + */ + +# tfdoc:file:description Stage 2s locals and resources. + +locals { + # read and decode factory files + _stage2_path = try( + pathexpand(var.factories_config.stage_2), null + ) + _stage2_files = try( + fileset(local._stage2_path, "**/*.yaml"), + [] + ) + _stage2_data = { + for f in local._stage2_files : + split(".", f)[0] => yamldecode(file( + "${coalesce(local._stage2_path, "-")}/${f}" + )) + } + # merge stage 2 from factory and variable data + _stage2 = merge( + # normalize factory data attributes with defaults and nulls + { + for k, v in local._stage2_data : k => { + short_name = lookup(v, "short_name", null) + cicd_config = lookup(v, "cicd_config", null) == null ? null : { + identity_provider = v.cicd_config.identity_provider + repository = merge(v.cicd_config.repository, { + branch = try(v.cicd_config.repository.branch, null) + type = try(v.cicd_config.repository.type, "github") + }) + } + folder_config = lookup(v, "folder_config", null) == null ? null : { + name = v.folder_config.name + create_env_folders = try(v.folder_config.create_env_folders, false) + iam = try(v.folder_config.iam, {}) + iam_bindings = try(v.folder_config.iam_bindings, {}) + iam_bindings_additive = try(v.folder_config.iam_bindings_additive, {}) + iam_by_principals = try(v.folder_config.iam_by_principals, {}) + org_policies = try(v.folder_config.org_policies, {}) + parent_id = try(v.folder_config.parent_id, null) + } + organization_config = { + iam = try(v.organization_config.iam, {}) + iam_bindings = try(v.organization_config.iam_bindings, {}) + iam_bindings_additive = try(v.organization_config.iam_bindings_additive, {}) + iam_by_principals = try(v.organization_config.iam_by_principals, {}) + } + stage3_config = { + iam_admin_delegated = try(v.stage3_config.iam_admin_delegated, []) + iam_viewer = try(v.stage3_config.iam_viewer, []) + } + } + }, + var.fast_stage_2 + ) + # normalize attributes + stage2 = { + for k, v in local._stage2 : k => merge(v, { + short_name = replace(coalesce(v.short_name, k), "_", "-") + folder_config = v.folder_config == null ? null : merge(v.folder_config, { + iam = { + for kk, vv in v.folder_config.iam : kk => [ + for m in vv : contains(["ro", "rw"], m) ? "${k}-${m}" : m + ] + } + iam_bindings = { + for kk, vv in v.folder_config.iam_bindings : + kk => { + role = vv.role + members = [ + for m in vv.members : contains(["ro", "rw"], m) ? "${k}-${m}" : m + ] + condition = lookup(vv, "condition", null) == null ? null : { + title = vv.condition.title + expression = templatestring(vv.condition.expression, { + organization = var.organization + tag_names = var.tag_names + tag_root = local.tag_root + }) + description = lookup(vv.condition, "description", null) + } + } + } + iam_bindings_additive = { + for kk, vv in v.folder_config.iam_bindings_additive : + kk => { + role = vv.role + member = contains(["ro", "rw"], vv.member) ? "${k}-${vv.member}" : vv.member + condition = lookup(vv, "condition", null) == null ? null : { + title = vv.condition.title + expression = templatestring(vv.condition.expression, { + organization = var.organization + tag_names = var.tag_names + tag_root = local.tag_root + }) + description = lookup(vv.condition, "description", null) + } + } + } + iam_by_principals = { + for kk, vv in v.folder_config.iam_by_principals : + (contains(["ro", "rw"], kk) ? "${k}-${kk}" : kk) => vv + } + }) + organization_config = merge(v.organization_config, { + iam_bindings_additive = { + for kk, vv in v.organization_config.iam_bindings_additive : kk => { + member = contains(["ro", "rw"], vv.member) ? "${k}-${vv.member}" : vv.member + role = vv.role + condition = lookup(vv, "condition", null) == null ? null : { + title = vv.condition.title + expression = templatestring(vv.condition.expression, { + organization = var.organization + tag_names = var.tag_names + tag_root = local.tag_root + }) + description = lookup(vv.condition, "description", null) + } + } + } + }) + }) + } + # environment folder permutations + stage2_env_folders = flatten([ + for k, v in local.stage2 : [ + for ek, ev in var.environments : { + key = "${k}-${ek}" + name = ev.name + stage = k + tag_name = ev.tag_name + } + ] if try(v.folder_config.create_env_folders, null) == true + ]) + # stage 2 short names used to detect overlap in stage 3s + stage2_shortnames = [for k, v in local.stage2 : v.short_name] +} + +# top-level folder + +module "stage2-folder" { + source = "../../../modules/folder" + for_each = { + for k, v in local.stage2 : k => v if v.folder_config != null + } + parent = ( + each.value.folder_config.parent_id == null + ? local.root_node + : try( + local.top_level_folder_ids[each.value.folder_config.parent_id], + each.value.folder_config.parent_id + ) + ) + name = each.value.folder_config.name + iam = { + for k, v in each.value.folder_config.iam : + lookup(var.custom_roles, k, k) => [ + for m in v : lookup(local.principals_iam, m, m) + ] + } + iam_bindings = { + for k, v in each.value.folder_config.iam_bindings : k => merge(v, { + members = [ + for m in v.members : lookup(local.principals_iam, m, m) + ] + role = lookup(var.custom_roles, v.role, v.role) + }) + } + iam_bindings_additive = { + for k, v in each.value.folder_config.iam_bindings_additive : k => merge(v, { + member = lookup(local.principals_iam, v.member, v.member) + role = lookup(var.custom_roles, v.role, v.role) + }) + } + iam_by_principals = { + for k, v in each.value.folder_config.iam_by_principals : + lookup(local.principals_iam, k, k) => [ + for r in v : lookup(var.custom_roles, r, r) + ] + } + org_policies = each.value.folder_config.org_policies + tag_bindings = { + context = local.tag_values["context/${each.key}"].id + } + depends_on = [module.top-level-folder] +} + +# optional per-environment folders + +module "stage2-folder-env" { + source = "../../../modules/folder" + for_each = { for k in local.stage2_env_folders : k.key => k } + parent = module.stage2-folder[each.value.stage].id + name = each.value.name + tag_bindings = { + environment = try( + local.tag_values["${var.tag_names.environment}/${each.value.tag_name}"].id, + null + ) + } +} + +# automation service accounts + +module "stage2-sa-rw" { + source = "../../../modules/iam-service-account" + for_each = local.stage2 + project_id = var.automation.project_id + name = templatestring(var.resource_names["sa-stage2_rw"], { + name = each.value.short_name + }) + display_name = ( + "Terraform resman ${each.key} service account." + ) + prefix = "${var.prefix}-${local.environment_default.short_name}" + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.cicd-sa-rw["${each.key}-prod"].iam_email, null) + ]) + } + iam_project_roles = { + (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectAdmin"] + } +} + +module "stage2-sa-ro" { + source = "../../../modules/iam-service-account" + for_each = local.stage2 + project_id = var.automation.project_id + name = templatestring(var.resource_names["sa-stage2_ro"], { + name = each.value.short_name + }) + display_name = ( + "Terraform resman ${each.key} service account (read-only)." + ) + prefix = "${var.prefix}-${local.environment_default.short_name}" + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.cicd-sa-ro["${each.key}-prod"].iam_email, null) + ]) + } + iam_project_roles = { + (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = [var.custom_roles["storage_viewer"]] + } +} + +# automation bucket + +module "stage2-bucket" { + source = "../../../modules/gcs" + for_each = local.stage2 + project_id = var.automation.project_id + name = templatestring(var.resource_names["gcs-stage2"], { + name = each.value.short_name + }) + prefix = "${var.prefix}-${local.environment_default.short_name}" + location = var.locations.gcs + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.stage2-sa-rw[each.key].iam_email] + "roles/storage.objectViewer" = [module.stage2-sa-ro[each.key].iam_email] + } +} diff --git a/fast/stages/1-resman/stage-3.tf b/fast/stages/1-resman/stage-3.tf index 6f86433a7..f0b9d13ea 100644 --- a/fast/stages/1-resman/stage-3.tf +++ b/fast/stages/1-resman/stage-3.tf @@ -23,20 +23,19 @@ locals { fileset(local._stage3_path, "**/*.yaml"), [] ) - _stage3 = { + _stage3_data = { for f in local._stage3_files : split(".", f)[0] => yamldecode(file( "${coalesce(local._stage3_path, "-")}/${f}" )) } # merge stage 3 from factory and variable data - stage3 = merge( + _stage3 = merge( # normalize factory data attributes with defaults and nulls { - for k, v in local._stage3 : k => { - short_name = v.short_name - environment = try(v.environment, "dev") - implements_stage = try(v.implements_stage, null) + for k, v in local._stage3_data : k => { + short_name = v.short_name + environment = try(v.environment, "dev") cicd_config = lookup(v, "cicd_config", null) == null ? null : { identity_provider = v.cicd_config.identity_provider repository = merge(v.cicd_config.repository, { @@ -45,69 +44,81 @@ locals { }) } folder_config = lookup(v, "folder_config", null) == null ? null : { - name = v.folder_config.name - iam_by_principals = try(v.folder_config.iam_by_principals, {}) - parent_id = try(v.folder_config.parent_id, null) - tag_bindings = try(v.folder_config.tag_bindings, {}) - } - organization_iam = lookup(v, "organization_iam", null) == null ? null : { - context_tag_value = v.organization_iam.context_tag_value - sa_roles = merge( - { ro = [], rw = [] }, v.organization_iam.sa_roles - ) - } - stage2_iam = { - networking = { - iam_admin_delegated = try( - v.stage2_iam.networking.iam_admin_delegated, false - ) - sa_roles = merge( - { ro = [], rw = [] }, try(v.stage2_iam.networking.sa_roles, {}) - ) - } - security = { - iam_admin_delegated = try( - v.stage2_iam.security.iam_admin_delegated, false - ) - sa_roles = merge( - { ro = [], rw = [] }, try(v.stage2_iam.security.sa_roles, {}) - ) - } + name = v.folder_config.name + iam = try(v.folder_config.iam, {}) + iam_bindings = try(v.folder_config.iam_bindings, {}) + iam_bindings_additive = try(v.folder_config.iam_bindings_additive, {}) + iam_by_principals = try(v.folder_config.iam_by_principals, {}) + org_policies = try(v.folder_config.org_policies, {}) + parent_id = try(v.folder_config.parent_id, null) + tag_bindings = try(v.folder_config.tag_bindings, {}) } } }, var.fast_stage_3 ) - # extract and normalize organization IAM for stage 3s - stage3_sa_roles_in_org = flatten([ - for k, v in local.stage3 : [ - for sa, roles in try(v.organization_iam.sa_roles, []) : [ - for role in roles : { - context = try(v.organization_iam.context_tag_value, "") - env = var.environments[v.environment].tag_name - role = role - sa = sa - s3 = k + # normalize attributes + stage3 = { + for k, v in local._stage3 : k => merge(v, { + short_name = replace(coalesce(v.short_name, k), "_", "-") + # this code is identical to the one used for stage 2s + folder_config = v.folder_config == null ? null : merge(v.folder_config, { + iam = { + for kk, vv in v.folder_config.iam : kk => [ + for m in vv : contains(["ro", "rw"], m) ? "${k}-${m}" : m + ] } - ] - ] - ]) - # extract and normalize stage 2 IAM for stage 2s - stage3_iam_in_stage2 = flatten([ - for k, v in local.stage3 : [ - for s2, attrs in v.stage2_iam : [ - for sa, roles in attrs.sa_roles : [ - for role in roles : { - env = var.environments[v.environment].tag_name - role = lookup(var.custom_roles, role, role) - sa = sa - s2 = s2 - s3 = k + iam_bindings = { + for kk, vv in v.folder_config.iam_bindings : + kk => { + role = vv.role + members = [ + for m in vv.members : contains(["ro", "rw"], m) ? "${k}-${m}" : m + ] + condition = vv.condition == null ? null : { + title = vv.condition.title + expression = templatestring(vv.condition.expression, { + organization = var.organization + tag_names = var.tag_names + tag_root = local.tag_root + }) + description = lookup(vv.condition, "description", null) + } } - ] - ] - ] - ]) + } + iam_bindings_additive = { + for kk, vv in v.folder_config.iam_bindings_additive : + kk => { + role = vv.role + member = contains(["ro", "rw"], vv.member) ? "${k}-${vv.member}" : vv.member + condition = vv.condition == null ? null : { + title = vv.condition.title + expression = templatestring(vv.condition.expression, { + organization = var.organization + tag_names = var.tag_names + tag_root = local.tag_root + }) + description = lookup(vv.condition, "description", null) + } + } + } + }) + }) + if !contains( + local.stage2_shortnames, replace(coalesce(v.short_name, k), "_", "-") + ) + } +} + +check "stage_short_names" { + assert { + condition = alltrue([ + for k, v in local._stage3 : !contains( + local.stage2_shortnames, replace(coalesce(v.short_name, k), "_", "-") + ) + ]) + error_message = "Some stage 3 short names overlap stage 2." + } } # top-level folder @@ -137,6 +148,7 @@ module "stage3-folder" { } iam_by_principals = each.value.folder_config.iam_by_principals + org_policies = each.value.folder_config.org_policies tag_bindings = merge( { environment = local.tag_values["environment/${var.environments[each.value.environment].tag_name}"].id @@ -162,7 +174,7 @@ module "stage3-sa-rw" { display_name = ( "Terraform resman ${each.key} service account." ) - prefix = "${var.prefix}-${each.value.environment}" + prefix = "${var.prefix}-${var.environments[each.value.environment].short_name}" iam = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.cicd-sa-rw["${each.key}-prod"].iam_email, null) diff --git a/fast/stages/1-resman/stage-cicd.tf b/fast/stages/1-resman/stage-cicd.tf index 58de5f8d9..2219aafaf 100644 --- a/fast/stages/1-resman/stage-cicd.tf +++ b/fast/stages/1-resman/stage-cicd.tf @@ -20,7 +20,7 @@ locals { _cicd_configs = merge( # stage 2 { - for k, v in var.fast_stage_2 : k => merge(v.cicd_config, { + for k, v in local.stage2 : k => merge(v.cicd_config, { env = "prod" level = 2 stage = replace(k, "_", "-") diff --git a/fast/stages/1-resman/top-level-folders.tf b/fast/stages/1-resman/top-level-folders.tf index 3f2df8ff5..2f4b62010 100644 --- a/fast/stages/1-resman/top-level-folders.tf +++ b/fast/stages/1-resman/top-level-folders.tf @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * 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. @@ -86,15 +86,17 @@ module "top-level-folder" { iam = { for role, members in each.value.iam : lookup(var.custom_roles, role, role) => [ - for member in members : (each.value.automation != null && member == "self") + for member in members : + (each.value.automation != null && member == "self") ? module.top-level-sa[each.key].iam_email - : lookup(local.top_level_sa, member, member) + : lookup(local.principals_iam, member, member) ] } iam_bindings = { for k, v in each.value.iam_bindings : k => { members = [ - for member in v.members : (each.value.automation != null && member == "self") + for member in v.members : + (each.value.automation != null && member == "self") ? module.top-level-sa[each.key].iam_email : lookup(local.top_level_sa, member, member) ] @@ -106,14 +108,20 @@ module "top-level-folder" { member = ( each.value.automation != null && v.member == "self" ? module.top-level-sa[each.key].iam_email - : lookup(local.top_level_sa, v.member, v.member) + : lookup(local.principals_iam, v.member, v.member) ) role = lookup(var.custom_roles, v.role, v.role) }) } - # we don't replace here to avoid dynamic values in keys - iam_by_principals = each.value.iam_by_principals - org_policies = each.value.org_policies + iam_by_principals = { + for k, v in each.value.iam_by_principals : + ( + (each.value.automation != null && k == "self") + ? module.top-level-sa[each.key].iam_email + : lookup(local.principals_iam, k, k) + ) => [for r in v : lookup(var.custom_roles, r, r)] + } + org_policies = each.value.org_policies tag_bindings = merge( # explicit tag bindings { diff --git a/fast/stages/1-resman/variables-stages.tf b/fast/stages/1-resman/variables-stages.tf index cecb50551..4a5db229c 100644 --- a/fast/stages/1-resman/variables-stages.tf +++ b/fast/stages/1-resman/variables-stages.tf @@ -16,56 +16,77 @@ variable "fast_stage_2" { description = "FAST stages 2 configurations." - type = object({ - networking = optional(object({ - enabled = optional(bool, true) - short_name = optional(string, "net") - cicd_config = optional(object({ - identity_provider = string - repository = object({ - name = string - branch = optional(string) - type = optional(string, "github") - }) - })) - folder_config = optional(object({ - create_env_folders = optional(bool, true) - iam_by_principals = optional(map(list(string)), {}) - name = optional(string, "Networking") - parent_id = optional(string) - }), {}) + type = map(object({ + short_name = optional(string) + cicd_config = optional(object({ + identity_provider = string + repository = object({ + name = string + branch = optional(string) + type = optional(string, "github") + }) + })) + folder_config = optional(object({ + name = string + parent_id = optional(string) + create_env_folders = optional(bool, true) + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(list(string)), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_by_principals = optional(map(list(string)), {}) + org_policies = optional(map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + rules = optional(list(object({ + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool) # for boolean policies only. + condition = optional(object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }), {}) + })), []) + })), {}) + })) + organization_config = optional(object({ + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_by_principals = optional(map(list(string)), {}) }), {}) - project_factory = optional(object({ - enabled = optional(bool, true) - short_name = optional(string, "pf") - cicd_config = optional(object({ - identity_provider = string - repository = object({ - name = string - branch = optional(string) - type = optional(string, "github") - }) - })) + stage3_config = optional(object({ + iam_admin_delegated = optional(list(object({ + environment = string + principal = string + })), []) + iam_viewer = optional(list(object({ + environment = string + principal = string + })), []) }), {}) - security = optional(object({ - enabled = optional(bool, true) - short_name = optional(string, "sec") - cicd_config = optional(object({ - identity_provider = string - repository = object({ - name = string - branch = optional(string) - type = optional(string, "github") - }) - })) - folder_config = optional(object({ - create_env_folders = optional(bool, false) - iam_by_principals = optional(map(list(string)), {}) - name = optional(string, "Security") - parent_id = optional(string) - }), {}) - }), {}) - }) + })) nullable = false default = {} validation { @@ -84,7 +105,7 @@ variable "fast_stage_3" { description = "FAST stages 3 configurations." # key is used for file names and loop keys and is like 'data-platfom-dev' type = map(object({ - short_name = string + short_name = optional(string) environment = optional(string, "dev") cicd_config = optional(object({ identity_provider = string @@ -95,42 +116,52 @@ variable "fast_stage_3" { }) })) folder_config = optional(object({ - name = string + name = string + parent_id = optional(string) + tag_bindings = optional(map(string), {}) + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(list(string)), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) iam_by_principals = optional(map(list(string)), {}) - parent_id = optional(string) - tag_bindings = optional(map(string), {}) + org_policies = optional(map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + rules = optional(list(object({ + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool) # for boolean policies only. + condition = optional(object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }), {}) + })), []) + })), {}) })) - organization_iam = optional(object({ - context_tag_value = string - sa_roles = object({ - ro = optional(list(string), []) - rw = optional(list(string), []) - }) - })) - stage2_iam = optional(object({ - networking = optional(object({ - iam_admin_delegated = optional(bool, false) - sa_roles = optional(object({ - ro = optional(list(string), []) - rw = optional(list(string), []) - }), {}) - }), {}) - security = optional(object({ - iam_admin_delegated = optional(bool, false) - sa_roles = optional(object({ - ro = optional(list(string), []) - rw = optional(list(string), []) - }), {}) - }), {}) - }), {}) })) nullable = false default = {} - # TODO: upgrade to cross-variable validation validation { condition = alltrue([ - for k, v in var.fast_stage_3 : - contains(["dev", "prod"], coalesce(v.environment, "-")) + for k, v in var.fast_stage_3 : contains( + keys(var.environments), + coalesce(v.environment, "-") + ) ]) error_message = "Invalid environment value." } diff --git a/fast/stages/1-resman/variables.tf b/fast/stages/1-resman/variables.tf index 6c88f052a..37ab5325f 100644 --- a/fast/stages/1-resman/variables.tf +++ b/fast/stages/1-resman/variables.tf @@ -20,7 +20,7 @@ variable "factories_config" { description = "Configuration for the resource factories or external data." type = object({ - org_policies = optional(string, "data/org-policies") + stage_2 = optional(string, "data/stage-2") stage_3 = optional(string, "data/stage-3") top_level_folders = optional(string, "data/top-level-folders") }) @@ -41,17 +41,12 @@ variable "resource_names" { gcs-nsec = optional(string, "resman-$${name}-0") gcs-pf = optional(string, "resman-$${name}-0") gcs-sec = optional(string, "prod-resman-$${name}-0") + gcs-stage2 = optional(string, "resman-$${name}-0") gcs-stage3 = optional(string, "resman-$${name}-0") sa-cicd_ro = optional(string, "resman-$${name}-1r") sa-cicd_rw = optional(string, "resman-$${name}-1") - sa-net_ro = optional(string, "prod-resman-$${name}-0r") - sa-net_rw = optional(string, "prod-resman-$${name}-0") - sa-pf_ro = optional(string, "resman-$${name}-0r") - sa-pf_rw = optional(string, "resman-$${name}-0") - sa-nsec_ro = optional(string, "resman-$${name}-0r") - sa-nsec_rw = optional(string, "resman-$${name}-0") - sa-sec_ro = optional(string, "prod-resman-$${name}-0r") - sa-sec_rw = optional(string, "prod-resman-$${name}-0") + sa-stage2_ro = optional(string, "resman-$${name}-0r") + sa-stage2_rw = optional(string, "resman-$${name}-0") sa-stage3_ro = optional(string, "resman-$${name}-0r") sa-stage3_rw = optional(string, "resman-$${name}-0") }) diff --git a/fast/stages/2-networking-a-simple/README.md b/fast/stages/2-networking-a-simple/README.md index 37dbc95b5..6eee9714c 100644 --- a/fast/stages/2-networking-a-simple/README.md +++ b/fast/stages/2-networking-a-simple/README.md @@ -513,7 +513,7 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [regions](variables.tf#L104) | Region definitions. | object({…}) | | {…} | | | [security_profile_groups](variables-fast.tf#L86) | Security profile group ids used for policy rule substitutions. | map(string) | | {} | 2-networking-ngfw | | [spoke_configs](variables.tf#L116) | Spoke connectivity configurations. | object({…}) | | {…} | | -| [stage_config](variables-fast.tf#L94) | FAST stage configuration. | object({…}) | | {} | 1-resman | +| [stage_config](variables-fast.tf#L94) | FAST stage configuration. | object({…}) | | {} | 1-resman | | [tag_values](variables-fast.tf#L108) | Root-level tag values. | map(string) | | {} | 1-resman | | [vpc_configs](variables.tf#L185) | Optional VPC network configurations. | object({…}) | | {} | | | [vpn_onprem_primary_config](variables.tf#L238) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | diff --git a/fast/stages/2-networking-a-simple/main.tf b/fast/stages/2-networking-a-simple/main.tf index 20796461a..d3666731b 100644 --- a/fast/stages/2-networking-a-simple/main.tf +++ b/fast/stages/2-networking-a-simple/main.tf @@ -30,11 +30,11 @@ locals { "roles/multiclusterservicediscovery.serviceAgent", "roles/vpcaccess.user", ])) - iam_delegated_principals = try( - var.stage_config["networking"].iam_delegated_principals, {} + iam_admin_delegated = try( + var.stage_config["networking"].iam_admin_delegated, {} ) - iam_viewer_principals = try( - var.stage_config["networking"].iam_viewer_principals, {} + iam_viewer = try( + var.stage_config["networking"].iam_viewer, {} ) # combine all regions from variables and subnets regions = distinct(concat( diff --git a/fast/stages/2-networking-a-simple/net-dev.tf b/fast/stages/2-networking-a-simple/net-dev.tf index d4776d0e0..6a7c4aa5b 100644 --- a/fast/stages/2-networking-a-simple/net-dev.tf +++ b/fast/stages/2-networking-a-simple/net-dev.tf @@ -59,13 +59,13 @@ module "dev-spoke-project" { metric_scopes = [module.landing-project.project_id] # optionally delegate a fixed set of IAM roles to selected principals iam = { - (var.custom_roles.project_iam_viewer) = try(local.iam_viewer_principals["dev"], []) + (var.custom_roles.project_iam_viewer) = try(local.iam_viewer["dev"], []) } iam_bindings = ( - lookup(local.iam_delegated_principals, "dev", null) == null ? {} : { + lookup(local.iam_admin_delegated, "dev", null) == null ? {} : { sa_delegated_grants = { role = "roles/resourcemanager.projectIamAdmin" - members = try(local.iam_delegated_principals["dev"], []) + members = try(local.iam_admin_delegated["dev"], []) condition = { title = "dev_stage3_sa_delegated_grants" description = "${var.environments["dev"].name} host project delegated grants." diff --git a/fast/stages/2-networking-a-simple/net-prod.tf b/fast/stages/2-networking-a-simple/net-prod.tf index 999d34368..1d3411ff9 100644 --- a/fast/stages/2-networking-a-simple/net-prod.tf +++ b/fast/stages/2-networking-a-simple/net-prod.tf @@ -59,13 +59,13 @@ module "prod-spoke-project" { metric_scopes = [module.landing-project.project_id] # optionally delegate a fixed set of IAM roles to selected principals iam = { - (var.custom_roles.project_iam_viewer) = try(local.iam_viewer_principals["prod"], []) + (var.custom_roles.project_iam_viewer) = try(local.iam_viewer["prod"], []) } iam_bindings = ( - lookup(local.iam_delegated_principals, "prod", null) == null ? {} : { + lookup(local.iam_admin_delegated, "prod", null) == null ? {} : { sa_delegated_grants = { role = "roles/resourcemanager.projectIamAdmin" - members = try(local.iam_delegated_principals["prod"], []) + members = try(local.iam_admin_delegated["prod"], []) condition = { title = "prod_stage3_sa_delegated_grants" description = "${var.environments["prod"].name} host project delegated grants." diff --git a/fast/stages/2-networking-a-simple/variables-fast.tf b/fast/stages/2-networking-a-simple/variables-fast.tf index c01b3f27f..56c29440b 100644 --- a/fast/stages/2-networking-a-simple/variables-fast.tf +++ b/fast/stages/2-networking-a-simple/variables-fast.tf @@ -96,9 +96,9 @@ variable "stage_config" { description = "FAST stage configuration." type = object({ networking = optional(object({ - short_name = optional(string) - iam_delegated_principals = optional(map(list(string)), {}) - iam_viewer_principals = optional(map(list(string)), {}) + short_name = optional(string) + iam_admin_delegated = optional(map(list(string)), {}) + iam_viewer = optional(map(list(string)), {}) }), {}) }) default = {} diff --git a/fast/stages/2-networking-b-nva/README.md b/fast/stages/2-networking-b-nva/README.md index 43a796f53..ec496e313 100644 --- a/fast/stages/2-networking-b-nva/README.md +++ b/fast/stages/2-networking-b-nva/README.md @@ -575,7 +575,7 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [psa_ranges](variables.tf#L112) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | {} | | | [regions](variables.tf#L132) | Region definitions. | object({…}) | | {…} | | | [security_profile_groups](variables-fast.tf#L86) | Security profile group ids used for policy rule substitutions. | map(string) | | {} | 2-networking-ngfw | -| [stage_config](variables-fast.tf#L94) | FAST stage configuration. | object({…}) | | {} | 1-resman | +| [stage_config](variables-fast.tf#L94) | FAST stage configuration. | object({…}) | | {} | 1-resman | | [tag_values](variables-fast.tf#L108) | Root-level tag values. | map(string) | | {} | 1-resman | | [vpc_configs](variables.tf#L144) | Optional VPC network configurations. | object({…}) | | {} | | | [vpn_onprem_primary_config](variables.tf#L227) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | diff --git a/fast/stages/2-networking-b-nva/main.tf b/fast/stages/2-networking-b-nva/main.tf index 2493c6dd8..447bb3b1a 100644 --- a/fast/stages/2-networking-b-nva/main.tf +++ b/fast/stages/2-networking-b-nva/main.tf @@ -29,11 +29,11 @@ locals { "roles/multiclusterservicediscovery.serviceAgent", "roles/vpcaccess.user", ])) - iam_delegated_principals = try( - var.stage_config["networking"].iam_delegated_principals, {} + iam_admin_delegated = try( + var.stage_config["networking"].iam_admin_delegated, {} ) - iam_viewer_principals = try( - var.stage_config["networking"].iam_viewer_principals, {} + iam_viewer = try( + var.stage_config["networking"].iam_viewer, {} ) # select the NVA ILB as next hop for spoke VPC routing depending on net mode nva_load_balancers = (var.network_mode == "ncc_ra") ? null : { diff --git a/fast/stages/2-networking-b-nva/net-dev.tf b/fast/stages/2-networking-b-nva/net-dev.tf index 5a95118a1..a6897d044 100644 --- a/fast/stages/2-networking-b-nva/net-dev.tf +++ b/fast/stages/2-networking-b-nva/net-dev.tf @@ -57,13 +57,13 @@ module "dev-spoke-project" { metric_scopes = [module.landing-project.project_id] # optionally delegate a fixed set of IAM roles to selected principals iam = { - (var.custom_roles.project_iam_viewer) = try(local.iam_viewer_principals["dev"], []) + (var.custom_roles.project_iam_viewer) = try(local.iam_viewer["dev"], []) } iam_bindings = ( - lookup(local.iam_delegated_principals, "dev", null) == null ? {} : { + lookup(local.iam_admin_delegated, "dev", null) == null ? {} : { sa_delegated_grants = { role = "roles/resourcemanager.projectIamAdmin" - members = try(local.iam_delegated_principals["dev"], []) + members = try(local.iam_admin_delegated["dev"], []) condition = { title = "dev_stage3_sa_delegated_grants" description = "${var.environments["dev"].name} host project delegated grants." diff --git a/fast/stages/2-networking-b-nva/net-prod.tf b/fast/stages/2-networking-b-nva/net-prod.tf index dfe9cdadb..c37a9a4d1 100644 --- a/fast/stages/2-networking-b-nva/net-prod.tf +++ b/fast/stages/2-networking-b-nva/net-prod.tf @@ -57,13 +57,13 @@ module "prod-spoke-project" { metric_scopes = [module.landing-project.project_id] # optionally delegate a fixed set of IAM roles to selected principals iam = { - (var.custom_roles.project_iam_viewer) = try(local.iam_viewer_principals["prod"], []) + (var.custom_roles.project_iam_viewer) = try(local.iam_viewer["prod"], []) } iam_bindings = ( - lookup(local.iam_delegated_principals, "prod", null) == null ? {} : { + lookup(local.iam_admin_delegated, "prod", null) == null ? {} : { sa_delegated_grants = { role = "roles/resourcemanager.projectIamAdmin" - members = try(local.iam_delegated_principals["prod"], []) + members = try(local.iam_admin_delegated["prod"], []) condition = { title = "prod_stage3_sa_delegated_grants" description = "${var.environments["prod"].name} host project delegated grants." diff --git a/fast/stages/2-networking-b-nva/variables-fast.tf b/fast/stages/2-networking-b-nva/variables-fast.tf index c01b3f27f..56c29440b 100644 --- a/fast/stages/2-networking-b-nva/variables-fast.tf +++ b/fast/stages/2-networking-b-nva/variables-fast.tf @@ -96,9 +96,9 @@ variable "stage_config" { description = "FAST stage configuration." type = object({ networking = optional(object({ - short_name = optional(string) - iam_delegated_principals = optional(map(list(string)), {}) - iam_viewer_principals = optional(map(list(string)), {}) + short_name = optional(string) + iam_admin_delegated = optional(map(list(string)), {}) + iam_viewer = optional(map(list(string)), {}) }), {}) }) default = {} diff --git a/fast/stages/2-networking-c-separate-envs/README.md b/fast/stages/2-networking-c-separate-envs/README.md index 97a97a80a..941eaa5b6 100644 --- a/fast/stages/2-networking-c-separate-envs/README.md +++ b/fast/stages/2-networking-c-separate-envs/README.md @@ -371,7 +371,7 @@ Regions are defined via the `regions` variable which sets up a mapping between t | [psa_ranges](variables.tf#L85) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | {} | | | [regions](variables.tf#L105) | Region definitions. | object({…}) | | {…} | | | [security_profile_groups](variables-fast.tf#L86) | Security profile group ids used for policy rule substitutions. | map(string) | | {} | 2-networking-ngfw | -| [stage_config](variables-fast.tf#L94) | FAST stage configuration. | object({…}) | | {} | 1-resman | +| [stage_config](variables-fast.tf#L94) | FAST stage configuration. | object({…}) | | {} | 1-resman | | [tag_values](variables-fast.tf#L108) | Root-level tag values. | map(string) | | {} | 1-resman | | [vpc_configs](variables.tf#L115) | Optional VPC network configurations. | object({…}) | | {} | | | [vpn_onprem_dev_primary_config](variables.tf#L153) | VPN gateway configuration for onprem interconnection from dev in the primary region. | object({…}) | | null | | diff --git a/fast/stages/2-networking-c-separate-envs/main.tf b/fast/stages/2-networking-c-separate-envs/main.tf index 954659093..5422cfaa4 100644 --- a/fast/stages/2-networking-c-separate-envs/main.tf +++ b/fast/stages/2-networking-c-separate-envs/main.tf @@ -30,11 +30,11 @@ locals { "roles/multiclusterservicediscovery.serviceAgent", "roles/vpcaccess.user", ])) - iam_delegated_principals = try( - var.stage_config["networking"].iam_delegated_principals, {} + iam_admin_delegated = try( + var.stage_config["networking"].iam_admin_delegated, {} ) - iam_viewer_principals = try( - var.stage_config["networking"].iam_viewer_principals, {} + iam_viewer = try( + var.stage_config["networking"].iam_viewer, {} ) # combine all regions from variables and subnets regions = distinct(concat( diff --git a/fast/stages/2-networking-c-separate-envs/net-dev.tf b/fast/stages/2-networking-c-separate-envs/net-dev.tf index 42b966070..a81084f32 100644 --- a/fast/stages/2-networking-c-separate-envs/net-dev.tf +++ b/fast/stages/2-networking-c-separate-envs/net-dev.tf @@ -59,13 +59,13 @@ module "dev-spoke-project" { } # optionally delegate a fixed set of IAM roles to selected principals iam = { - (var.custom_roles.project_iam_viewer) = try(local.iam_viewer_principals["dev"], []) + (var.custom_roles.project_iam_viewer) = try(local.iam_viewer["dev"], []) } iam_bindings = ( - lookup(local.iam_delegated_principals, "dev", null) == null ? {} : { + lookup(local.iam_admin_delegated, "dev", null) == null ? {} : { sa_delegated_grants = { role = "roles/resourcemanager.projectIamAdmin" - members = try(local.iam_delegated_principals["dev"], []) + members = try(local.iam_admin_delegated["dev"], []) condition = { title = "dev_stage3_sa_delegated_grants" description = "${var.environments["dev"].name} host project delegated grants." diff --git a/fast/stages/2-networking-c-separate-envs/net-prod.tf b/fast/stages/2-networking-c-separate-envs/net-prod.tf index 77c730381..9f7ce84d9 100644 --- a/fast/stages/2-networking-c-separate-envs/net-prod.tf +++ b/fast/stages/2-networking-c-separate-envs/net-prod.tf @@ -59,13 +59,13 @@ module "prod-spoke-project" { } # optionally delegate a fixed set of IAM roles to selected principals iam = { - (var.custom_roles.project_iam_viewer) = try(local.iam_viewer_principals["prod"], []) + (var.custom_roles.project_iam_viewer) = try(local.iam_viewer["prod"], []) } iam_bindings = ( - lookup(local.iam_delegated_principals, "prod", null) == null ? {} : { + lookup(local.iam_admin_delegated, "prod", null) == null ? {} : { sa_delegated_grants = { role = "roles/resourcemanager.projectIamAdmin" - members = try(local.iam_delegated_principals["prod"], []) + members = try(local.iam_admin_delegated["prod"], []) condition = { title = "prod_stage3_sa_delegated_grants" description = "${var.environments["prod"].name} host project delegated grants." diff --git a/fast/stages/2-networking-c-separate-envs/variables-fast.tf b/fast/stages/2-networking-c-separate-envs/variables-fast.tf index c01b3f27f..56c29440b 100644 --- a/fast/stages/2-networking-c-separate-envs/variables-fast.tf +++ b/fast/stages/2-networking-c-separate-envs/variables-fast.tf @@ -96,9 +96,9 @@ variable "stage_config" { description = "FAST stage configuration." type = object({ networking = optional(object({ - short_name = optional(string) - iam_delegated_principals = optional(map(list(string)), {}) - iam_viewer_principals = optional(map(list(string)), {}) + short_name = optional(string) + iam_admin_delegated = optional(map(list(string)), {}) + iam_viewer = optional(map(list(string)), {}) }), {}) }) default = {} diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md index 026a5c0bc..824d547b1 100644 --- a/fast/stages/2-security/README.md +++ b/fast/stages/2-security/README.md @@ -293,7 +293,7 @@ tls_inspection = { | [essential_contacts](variables.tf#L98) | Email used for essential contacts, unset if null. | string | | null | | | [kms_keys](variables.tf#L104) | KMS keys to create, keyed by name. | map(object({…})) | | {} | | | [outputs_location](variables.tf#L142) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [stage_config](variables-fast.tf#L85) | FAST stage configuration. | object({…}) | | {} | 1-resman | +| [stage_config](variables-fast.tf#L85) | FAST stage configuration. | object({…}) | | {} | 1-resman | | [tag_values](variables-fast.tf#L99) | Root-level tag values. | map(string) | | {} | 1-resman | ## Outputs diff --git a/fast/stages/2-security/main.tf b/fast/stages/2-security/main.tf index 644f5b4ab..7042ae4af 100644 --- a/fast/stages/2-security/main.tf +++ b/fast/stages/2-security/main.tf @@ -19,11 +19,11 @@ locals { iam_delegated = join(",", formatlist("'%s'", [ "roles/cloudkms.cryptoKeyEncrypterDecrypter" ])) - iam_delegated_principals = try( - var.stage_config["security"].iam_delegated_principals, {} + iam_admin_delegated = try( + var.stage_config["security"].iam_admin_delegated, {} ) - iam_viewer_principals = try( - var.stage_config["security"].iam_viewer_principals, {} + iam_viewer = try( + var.stage_config["security"].iam_viewer, {} ) project_services = [ "certificatemanager.googleapis.com", @@ -64,14 +64,14 @@ module "project" { # optionally delegate a fixed set of IAM roles to selected principals iam = { (var.custom_roles.project_iam_viewer) = try( - local.iam_viewer_principals[each.key], [] + local.iam_viewer[each.key], [] ) } iam_bindings = ( - lookup(local.iam_delegated_principals, each.key, null) == null ? {} : { + lookup(local.iam_admin_delegated, each.key, null) == null ? {} : { sa_delegated_grants = { role = "roles/resourcemanager.projectIamAdmin" - members = try(local.iam_delegated_principals[each.key], []) + members = try(local.iam_admin_delegated[each.key], []) condition = { title = "${each.key}_stage3_sa_delegated_grants" description = "${var.environments[each.key].name} project delegated grants." diff --git a/fast/stages/2-security/variables-fast.tf b/fast/stages/2-security/variables-fast.tf index 86abeefb3..be53b32f8 100644 --- a/fast/stages/2-security/variables-fast.tf +++ b/fast/stages/2-security/variables-fast.tf @@ -87,9 +87,9 @@ variable "stage_config" { description = "FAST stage configuration." type = object({ security = optional(object({ - short_name = optional(string) - iam_delegated_principals = optional(map(list(string)), {}) - iam_viewer_principals = optional(map(list(string)), {}) + short_name = optional(string) + iam_admin_delegated = optional(map(list(string)), {}) + iam_viewer = optional(map(list(string)), {}) }), {}) }) default = {} diff --git a/fast/stages/3-gcve-dev/README.md b/fast/stages/3-gcve-dev/README.md index 611b844f6..85dcca58a 100644 --- a/fast/stages/3-gcve-dev/README.md +++ b/fast/stages/3-gcve-dev/README.md @@ -13,6 +13,7 @@ The setup configured here is for a single environment in a single region, and is - [Single-region per-environment GCVE deployment](#single-region-per-environment-gcve-deployment) - [Multi-regional deployments](#multi-regional-deployments) - [How to run this stage](#how-to-run-this-stage) + - [Resource management configuration](#resource-management-configuration) - [Provider and Terraform variables](#provider-and-terraform-variables) - [Impersonating the automation service account](#impersonating-the-automation-service-account) - [Variable configuration](#variable-configuration) @@ -86,6 +87,67 @@ It is also possible to run this stage in isolation. Refer to the *[Running in is Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. +### Resource management configuration + +Some configuration changes are needed in resource management before this stage can be run. + +First, define a parent folder for each stage environment folder in the `data/top-level-folder` folder [in the resource management stage](../1-resman/data/top-level-folders/). As an example, this YAML definition creates a `GCVE` folder under the organization: + +```yaml +# yaml-language-server: $schema=../../schemas/top-level-folder.schema.json + +name: GCVE + +# IAM bindings and organization policies can also be defined here +``` + +Then, make sure the stage 3 is enabled in the `data/stage-3` folder [in the resource management stage](../1-resman/data/stage-3/). As an example, this YAML definition saved as `gcve-dev.yaml` enables this stage 3 for the development environment: + +```yaml +# yaml-language-server: $schema=../../schemas/fast-stage3.schema.json + +short_name: gcve +environment: dev +folder_config: + name: Development + parent_id: gcve +``` + +Then edit the definition of the networking stage 2 in the `data/stage2` folder [in the resource management stage](../1-resman/data/stage-2/) to include the IAM configuration for GCVE. The following are example snippets for GCVE dev, make sure they match the `short_name` and `environment` configured above. + +In `folder_config.iam_bindings_additive` add: + +```yaml +# folder_config: + # iam_bindings_additive: + gcve_dev_net_admin: + role: gcve_network_admin + member: gcve-dev-rw + condition: + title: GCVE dev network admin. + expression: | + resource.matchTag('${organization.id}/${tag_names.environment}', 'development') + gcve_dev_net_viewer: + role: gcve_network_viewer + member: gcve-dev-ro + condition: + title: GCVE dev network viewer. + expression: | + resource.matchTag('${organization.id}/${tag_names.environment}', 'development') +``` + +In `stage3_config` add the following so that the networking stage grants IAM delegated permissions to this stage's service accounts: + +```yaml +# stage3_config: + iam_admin_delegated: + - environment: dev + principal: gcve-dev-rw + iam_viewer: + - environment: dev + principal: gcve-dev-ro +``` + ### Provider and Terraform variables As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. diff --git a/fast/stages/3-gke-dev/README.md b/fast/stages/3-gke-dev/README.md index 790fecd88..1d642b885 100644 --- a/fast/stages/3-gke-dev/README.md +++ b/fast/stages/3-gke-dev/README.md @@ -11,6 +11,7 @@ The following diagram illustrates the high-level design of created resources, wh - [Design overview and choices](#design-overview-and-choices) - [How to run this stage](#how-to-run-this-stage) + - [Resource management configuration](#resource-management-configuration) - [Provider and Terraform variables](#provider-and-terraform-variables) - [Impersonating the automation service account](#impersonating-the-automation-service-account) - [Variable configuration](#variable-configuration) @@ -59,6 +60,43 @@ It's of course possible to run this stage in isolation, refer to the *[Running i Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. +### Resource management configuration + +Some configuration changes are needed in resource management before this stage can be run. + +First, define a parent folder for each stage environment folder in the `data/top-level-folder` folder [in the resource management stage](../1-resman/data/top-level-folders/). As an example, this YAML definition creates a `GKE` folder under the organization: + +```yaml +# yaml-language-server: $schema=../../schemas/top-level-folder.schema.json + +name: GKE + +# IAM bindings and organization policies can also be defined here +``` + +Then, edit the definition of the networking stage 2 in the `data/stage2` folder [in the resource management stage](../1-resman/data/stage-2/) to include the IAM configuration for GKE. The following are example snippets for GKE dev, make sure they match the `short_name` and `environment` configured above. + +In `folder_config.iam_bindings_additive` add: + +```yaml +# folder_config: + # iam_bindings_additive: + gke_dns_admin: + role: roles/dns.admin + member: gke-dev-ro + condition: + title: GKE dev DNS admin. + expression: | + resource.matchTag('${organization.id}/${tag_names.environment}', 'development') + gke_dns_reader: + role: roles/dns.reader + member: gke-dev-ro + condition: + title: GKE dev DNS reader. + expression: | + resource.matchTag('${organization.id}/${tag_names.environment}', 'development') +``` + ### Provider and Terraform variables As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. diff --git a/tests/collectors.py b/tests/collectors.py index 1e4932058..66ac5c54e 100644 --- a/tests/collectors.py +++ b/tests/collectors.py @@ -72,6 +72,7 @@ class FabricTestFile(pytest.File): common = raw.pop('common_tfvars', []) for test_name, spec in raw.get('tests', {}).items(): spec = {} if spec is None else spec + extra_dirs = spec.get('extra_dirs') extra_files = spec.get('extra_files') inventories = spec.get('inventory', [f'{test_name}.yaml']) tf_var_files = common + [f'{test_name}.tfvars'] + spec.get('tfvars', []) @@ -82,24 +83,26 @@ class FabricTestFile(pytest.File): yield FabricTestItem.from_parent(self, name=name, module=module, inventory=[i], tf_var_files=tf_var_files, - extra_files=extra_files) + extra_files=extra_files, + extra_dirs=extra_dirs) class FabricTestItem(pytest.Item): def __init__(self, name, parent, module, inventory, tf_var_files, - extra_files=None): + extra_files=None, extra_dirs=None): super().__init__(name, parent) self.module = module self.inventory = inventory self.tf_var_files = tf_var_files + self.extra_dirs = extra_dirs self.extra_files = extra_files def runtest(self): try: summary = plan_validator(self.module, self.inventory, self.parent.path.parent, self.tf_var_files, - self.extra_files) + self.extra_files, self.extra_dirs) except AssertionError: def full_paths(x): diff --git a/tests/fast/stages/s1_resman/simple.tfvars b/tests/fast/stages/s1_resman/simple.tfvars index 97adcfbdf..66e114a88 100644 --- a/tests/fast/stages/s1_resman/simple.tfvars +++ b/tests/fast/stages/s1_resman/simple.tfvars @@ -1,3 +1,67 @@ +# stage variables + +fast_addon = { + ngfw = { + parent_stage = "2-networking" + } +} +fast_stage_2 = { + # replicate one stage 2 via tfvars so as to check CI/CD configuration + project-factory = { + short_name = "pf" + cicd_config = { + identity_provider = "gh-test" + repository = { + name = "cloud-foundation-fabric/1-resman" + branch = "main" + } + } + organization_config = { + iam_bindings_additive = { + sa_pf_conditional_org_policy = { + member = "rw" + role = "roles/orgpolicy.policyAdmin" + condition = { + title = "org_policy_tag_pf_scoped" + description = "Org policy tag scoped grant for project factory." + expression = "resource.matchTag('$${organization.id}/$${tag_names.context}', 'project-factory')" + } + } + } + } + } +} +tags = { + context = { + values = { + data-platform = {} + gcve = {} + gke = {} + nsec = {} + sandbox = {} + } + } + environment = { + values = { + development = { + iam = { + "roles/resourcemanager.tagUser" = ["gcve-dev-rw"] + "roles/resourcemanager.tagViewer" = ["gcve-dev-ro"] + } + } + } + } +} +top_level_folders = { + tenants = { + name = "Tenants" + iam_by_principals = {} + } + shared = { + name = "Shared Infrastructure" + } +} + # globals billing_account = { @@ -82,71 +146,3 @@ custom_roles = { logging = { project_id = "fast-prod-log-audit-0" } - -# stage variables - -fast_addon = { - ngfw = { - parent_stage = "2-networking" - } -} -fast_stage_2 = { - networking = { - cicd_config = { - identity_provider = "gh-test" - repository = { - branch = "main" - name = "test/00-networking" - type = "github" - } - } - folder_config = { - parent_id = "shared" - } - } - security = { - cicd_config = { - identity_provider = "gl-test" - repository = { - name = "test/00-security" - type = "gitlab" - } - } - } -} -tags = { - context = { - values = { - data-platform = {} - gcve = {} - gke = {} - nsec = {} - sandbox = {} - } - } - environment = { - values = { - development = { - iam = { - "roles/resourcemanager.tagUser" = ["project-factory-dev"] - "roles/resourcemanager.tagViewer" = ["project-factory-dev-r"] - } - } - production = { - iam = { - "roles/resourcemanager.tagUser" = ["project-factory-prod"] - "roles/resourcemanager.tagViewer" = ["project-factory-prod-r"] - } - } - } - } -} -top_level_folders = { - tenants = { - name = "Tenants" - iam_by_principals = {} - } - shared = { - name = "Shared Infrastructure" - } -} diff --git a/tests/fast/stages/s1_resman/simple.yaml b/tests/fast/stages/s1_resman/simple.yaml index 35fb34769..1d5d5cc4e 100644 --- a/tests/fast/stages/s1_resman/simple.yaml +++ b/tests/fast/stages/s1_resman/simple.yaml @@ -12,133 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. -values: - module.cicd-sa-ro["networking"].google_project_iam_member.project-roles["fast2-prod-automation-roles/logging.logWriter"]: - condition: [] - project: fast2-prod-automation - role: roles/logging.logWriter - module.cicd-sa-ro["networking"].google_service_account.service_account[0]: - account_id: fast2-prod-resman-net-1r - create_ignore_already_exists: null - description: null - disabled: false - display_name: CI/CD 2-net prod service account (read-only). - email: fast2-prod-resman-net-1r@fast2-prod-automation.iam.gserviceaccount.com - member: serviceAccount:fast2-prod-resman-net-1r@fast2-prod-automation.iam.gserviceaccount.com - project: fast2-prod-automation - timeouts: null - module.cicd-sa-ro["networking"].google_service_account_iam_binding.authoritative["roles/iam.workloadIdentityUser"]: - condition: [] - members: - - principalSet://iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/ldj-bootstrap/attribute.repository/test/00-networking - role: roles/iam.workloadIdentityUser - ? module.cicd-sa-ro["networking"].google_storage_bucket_iam_member.bucket-roles["fast2-prod-iac-core-outputs-roles/storage.objectViewer"] - : bucket: fast2-prod-iac-core-outputs - condition: [] - role: roles/storage.objectViewer - module.cicd-sa-ro["security"].google_project_iam_member.project-roles["fast2-prod-automation-roles/logging.logWriter"]: - condition: [] - project: fast2-prod-automation - role: roles/logging.logWriter - module.cicd-sa-ro["security"].google_service_account.service_account[0]: - account_id: fast2-prod-resman-sec-1r - create_ignore_already_exists: null - description: null - disabled: false - display_name: CI/CD 2-sec prod service account (read-only). - email: fast2-prod-resman-sec-1r@fast2-prod-automation.iam.gserviceaccount.com - member: serviceAccount:fast2-prod-resman-sec-1r@fast2-prod-automation.iam.gserviceaccount.com - project: fast2-prod-automation - timeouts: null - module.cicd-sa-ro["security"].google_service_account_iam_binding.authoritative["roles/iam.workloadIdentityUser"]: - condition: [] - members: - - principalSet://iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/ldj-bootstrap/attribute.repository/test/00-security - role: roles/iam.workloadIdentityUser - ? module.cicd-sa-ro["security"].google_storage_bucket_iam_member.bucket-roles["fast2-prod-iac-core-outputs-roles/storage.objectViewer"] - : bucket: fast2-prod-iac-core-outputs - condition: [] - role: roles/storage.objectViewer - module.cicd-sa-rw["networking"].google_project_iam_member.project-roles["fast2-prod-automation-roles/logging.logWriter"]: - condition: [] - project: fast2-prod-automation - role: roles/logging.logWriter - module.cicd-sa-rw["networking"].google_service_account.service_account[0]: - account_id: fast2-prod-resman-net-1 - create_ignore_already_exists: null - description: null - disabled: false - display_name: CI/CD 2-net prod service account. - email: fast2-prod-resman-net-1@fast2-prod-automation.iam.gserviceaccount.com - member: serviceAccount:fast2-prod-resman-net-1@fast2-prod-automation.iam.gserviceaccount.com - project: fast2-prod-automation - timeouts: null - module.cicd-sa-rw["networking"].google_service_account_iam_binding.authoritative["roles/iam.workloadIdentityUser"]: - condition: [] - members: - - principalSet://iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/ldj-bootstrap/attribute.fast_sub/repo:test/00-networking:ref:refs/heads/main - role: roles/iam.workloadIdentityUser - ? module.cicd-sa-rw["networking"].google_storage_bucket_iam_member.bucket-roles["fast2-prod-iac-core-outputs-roles/storage.objectViewer"] - : bucket: fast2-prod-iac-core-outputs - condition: [] - role: roles/storage.objectViewer - module.cicd-sa-rw["security"].google_project_iam_member.project-roles["fast2-prod-automation-roles/logging.logWriter"]: - condition: [] - project: fast2-prod-automation - role: roles/logging.logWriter - module.cicd-sa-rw["security"].google_service_account.service_account[0]: - account_id: fast2-prod-resman-sec-1 - create_ignore_already_exists: null - description: null - disabled: false - display_name: CI/CD 2-sec prod service account. - email: fast2-prod-resman-sec-1@fast2-prod-automation.iam.gserviceaccount.com - member: serviceAccount:fast2-prod-resman-sec-1@fast2-prod-automation.iam.gserviceaccount.com - project: fast2-prod-automation - timeouts: null - module.cicd-sa-rw["security"].google_service_account_iam_binding.authoritative["roles/iam.workloadIdentityUser"]: - condition: [] - members: - - principalSet://iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/ldj-bootstrap/attribute.repository/test/00-security - role: roles/iam.workloadIdentityUser - ? module.cicd-sa-rw["security"].google_storage_bucket_iam_member.bucket-roles["fast2-prod-iac-core-outputs-roles/storage.objectViewer"] - : bucket: fast2-prod-iac-core-outputs - condition: [] - role: roles/storage.objectViewer - counts: - google_folder: 14 - google_folder_iam_binding: 74 + google_folder: 12 + google_folder_iam_binding: 50 google_org_policy_policy: 2 - google_organization_iam_member: 18 - google_project_iam_member: 23 - google_service_account: 23 - google_service_account_iam_binding: 23 - google_storage_bucket: 10 - google_storage_bucket_iam_binding: 20 - google_storage_bucket_iam_member: 23 - google_storage_bucket_object: 24 - google_tags_tag_binding: 14 + google_organization_iam_member: 15 + google_project_iam_member: 13 + google_service_account: 13 + google_service_account_iam_binding: 13 + google_storage_bucket: 6 + google_storage_bucket_iam_binding: 12 + google_storage_bucket_iam_member: 13 + google_storage_bucket_object: 15 + google_tags_tag_binding: 12 google_tags_tag_key: 2 google_tags_tag_value: 12 google_tags_tag_value_iam_binding: 4 - modules: 48 - resources: 286 + modules: 32 + resources: 194 outputs: cicd_repositories: - networking: + project-factory: provider: projects/1234567890/locations/global/workloadIdentityPools/ldj-bootstrap/providers/ldj-bootstrap-github-ludomagno repository: branch: main - name: test/00-networking + name: cloud-foundation-fabric/1-resman type: github - security: - provider: projects/1234567890/locations/global/workloadIdentityPools/ldj-bootstrap/providers/ldj-bootstrap-gitlab-ludomagno - repository: - branch: null - name: test/00-security - type: gitlab - folder_ids: __missing__ - tfvars: __missing__ + service_accounts: + gcve-dev-ro: fast2-dev-resman-gcve-0r@fast2-prod-automation.iam.gserviceaccount.com + gcve-dev-rw: fast2-dev-resman-gcve-0@fast2-prod-automation.iam.gserviceaccount.com + gke-dev-ro: fast2-dev-resman-gke-0r@fast2-prod-automation.iam.gserviceaccount.com + gke-dev-rw: fast2-dev-resman-gke-0@fast2-prod-automation.iam.gserviceaccount.com + networking-ro: fast2-prod-resman-net-0r@fast2-prod-automation.iam.gserviceaccount.com + networking-rw: fast2-prod-resman-net-0@fast2-prod-automation.iam.gserviceaccount.com + project-factory-ro: fast2-prod-resman-pf-0r@fast2-prod-automation.iam.gserviceaccount.com + project-factory-rw: fast2-prod-resman-pf-0@fast2-prod-automation.iam.gserviceaccount.com + sandbox: fast2-dev-resman-sbox-0@fast2-prod-automation.iam.gserviceaccount.com + security-ro: fast2-prod-resman-sec-0r@fast2-prod-automation.iam.gserviceaccount.com + security-rw: fast2-prod-resman-sec-0@fast2-prod-automation.iam.gserviceaccount.com diff --git a/tests/fast/stages/s1_resman/tftest.yaml b/tests/fast/stages/s1_resman/tftest.yaml index c09a159a6..739d91304 100644 --- a/tests/fast/stages/s1_resman/tftest.yaml +++ b/tests/fast/stages/s1_resman/tftest.yaml @@ -16,3 +16,5 @@ module: fast/stages/1-resman tests: simple: + # extra_dirs: + # - ../../../tests/fast/stages/s1_resman/test-data diff --git a/tests/fast/stages/s2_networking_a_simple/simple.yaml b/tests/fast/stages/s2_networking_a_simple/simple.yaml index ed6159b6a..20b2edaf7 100644 --- a/tests/fast/stages/s2_networking_a_simple/simple.yaml +++ b/tests/fast/stages/s2_networking_a_simple/simple.yaml @@ -39,7 +39,7 @@ counts: google_monitoring_dashboard: 3 google_monitoring_monitored_project: 2 google_project: 3 - google_project_iam_binding: 4 + google_project_iam_binding: 2 google_project_iam_member: 20 google_project_service: 26 google_project_service_identity: 20 @@ -47,4 +47,4 @@ counts: google_tags_tag_binding: 3 modules: 27 random_id: 3 - resources: 194 + resources: 192 diff --git a/tests/fast/stages/s2_security/simple.yaml b/tests/fast/stages/s2_security/simple.yaml index 2b624399e..fdea0fb7d 100644 --- a/tests/fast/stages/s2_security/simple.yaml +++ b/tests/fast/stages/s2_security/simple.yaml @@ -20,14 +20,14 @@ counts: google_privateca_ca_pool: 1 google_privateca_certificate_authority: 1 google_project: 2 - google_project_iam_binding: 4 + google_project_iam_binding: 2 google_project_iam_member: 4 google_project_service: 10 google_project_service_identity: 8 google_storage_bucket_object: 1 google_tags_tag_binding: 2 modules: 12 - resources: 58 + resources: 56 outputs: certificate_authority_pools: __missing__ diff --git a/tests/fixtures.py b/tests/fixtures.py index c1ab300e7..58846c34f 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -64,7 +64,7 @@ def _prepare_root_module(path): def plan_summary(module_path, basedir, tf_var_files=None, extra_files=None, - **tf_vars): + extra_dirs=None, **tf_vars): """ Run a Terraform plan on the module located at `module_path`. @@ -105,7 +105,11 @@ def plan_summary(module_path, basedir, tf_var_files=None, extra_files=None, extra_files = [(module_path / filename).resolve() for x in extra_files or [] for filename in glob.glob(x, root_dir=module_path)] - tf.setup(extra_files=extra_files, upgrade=True) + extra_dirs = [ + (module_path / dirname).resolve() for dirname in extra_dirs or [] + ] + tf.setup(extra_files=extra_files + extra_dirs, upgrade=True) + # raise SystemExit(extra_dirs) tf_var_files = [(basedir / x).resolve() for x in tf_var_files or []] plan = tf.plan(output=True, tf_var_file=tf_var_files, tf_vars=tf_vars) @@ -148,20 +152,21 @@ def plan_summary_fixture(request): """ def inner(module_path, basedir=None, tf_var_files=None, extra_files=None, - **tf_vars): + extra_dirs=None, **tf_vars): if basedir is None: basedir = Path(request.fspath).parent return plan_summary(module_path=module_path, basedir=basedir, tf_var_files=tf_var_files, extra_files=extra_files, - **tf_vars) + extra_dirs=extra_dirs, **tf_vars) return inner def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, - extra_files=None, **tf_vars): + extra_files=None, extra_dirs=None, **tf_vars): summary = plan_summary(module_path=module_path, tf_var_files=tf_var_files, - extra_files=extra_files, basedir=basedir, **tf_vars) + extra_files=extra_files, extra_dirs=extra_dirs, + basedir=basedir, **tf_vars) # allow single single string for inventory_paths if not isinstance(inventory_paths, list):