diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 21c0746a..1c63b78d 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -53,12 +53,12 @@ jobs: run: | terraform fmt -recursive -check -diff $GITHUB_WORKSPACE - - name: Check documentation (fabric) + - name: Check documentation id: documentation-fabric run: | - python3 tools/check_documentation.py examples modules fast + python3 tools/check_documentation.py modules fast blueprints - - name: Check documentation links (fabric) + - name: Check documentation links id: documentation-links-fabric run: | python3 tools/check_links.py . diff --git a/CHANGELOG.md b/CHANGELOG.md index 65841252..d438ea2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. ### BLUEPRINTS +- [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#932](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/932)] feat(project-factory): introduce additive iam bindings to project-fac… ([Malet](https://github.com/Malet)) +- [[#925](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/925)] Network dashboard: update main.tf and README following #922 ([brianhmj](https://github.com/brianhmj)) - [[#924](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/924)] Fix formatting for gcloud dataflow job launch command ([aymanfarhat](https://github.com/aymanfarhat)) - [[#921](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/921)] Align documentation, move glb blueprint ([ludoo](https://github.com/ludoo)) - [[#915](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/915)] TFE OIDC with GCP WIF blueprint added ([averbuks](https://github.com/averbuks)) @@ -51,6 +54,9 @@ All notable changes to this project will be documented in this file. ### FAST +- [[#935](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/935)] FAST: enable org policy API, fix run.allowedIngress value ([ludoo](https://github.com/ludoo)) +- [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#930](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/930)] **incompatible change:** Update organization/folder/project modules to use new org policies API and tf1.3 optionals ([juliocc](https://github.com/juliocc)) - [[#911](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/911)] FAST: Additional PGA DNS records ([sruffilli](https://github.com/sruffilli)) - [[#903](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/903)] Initial replacement for CI/CD stage ([ludoo](https://github.com/ludoo)) - [[#898](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/898)] Update FAST bootstrap README.md ([juliocc](https://github.com/juliocc)) @@ -71,6 +77,8 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#930](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/930)] **incompatible change:** Update organization/folder/project modules to use new org policies API and tf1.3 optionals ([juliocc](https://github.com/juliocc)) - [[#926](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/926)] Fix backwards compatibility for vpc subnet descriptions ([ludoo](https://github.com/ludoo)) - [[#927](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/927)] Add support for deployment type and api proxy type for Apigee org ([kmucha555](https://github.com/kmucha555)) - [[#923](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/923)] Fix service account creation error in gke nodepool module ([ludoo](https://github.com/ludoo)) diff --git a/blueprints/README.md b/blueprints/README.md index 77e13906..bfb4c483 100644 --- a/blueprints/README.md +++ b/blueprints/README.md @@ -1,12 +1,12 @@ # Terraform end-to-end blueprints for Google Cloud -This section **[networking blueprints](./networking/)** that implement core patterns or features, **[data solutions blueprints](./data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations blueprints](./cloud-operations/)** that leverage specific products to meet specific operational needs, **[GKE](./gke/)** and **[Serverless](./serverless/)** blueprints, and **[factories](./factories/)** that implement resource factories for the repetitive creation of specific resources. +This section provides **[networking blueprints](./networking/)** that implement core patterns or features, **[data solutions blueprints](./data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations blueprints](./cloud-operations/)** that leverage specific products to meet specific operational needs, **[GKE](./gke/)** and **[Serverless](./serverless/)** blueprints, and **[factories](./factories/)** that implement resource factories for the repetitive creation of specific resources. Currently available blueprints: - **cloud operations** - [Active Directory Federation Services](./cloud-operations/adfs), [Cloud Asset Inventory feeds for resource change tracking and remediation](./cloud-operations/asset-inventory-feed-remediation), [Fine-grained Cloud DNS IAM via Service Directory](./cloud-operations/dns-fine-grained-iam), [Cloud DNS & Shared VPC design](./cloud-operations/dns-shared-vpc), [Delegated Role Grants](./cloud-operations/iam-delegated-role-grants), [Networking Dashboard](./cloud-operations/network-dashboard), [Managing on-prem service account keys by uploading public keys](./cloud-operations/onprem-sa-key-management), [Compute Image builder with Hashicorp Packer](./cloud-operations/packer-image-builder), [Packer example](./cloud-operations/packer-image-builder/packer), [Compute Engine quota monitoring](./cloud-operations/quota-monitoring), [Scheduled Cloud Asset Inventory Export to Bigquery](./cloud-operations/scheduled-asset-inventory-export-bq), [Configuring workload identity federation for Terraform Cloud/Enterprise workflow](./cloud-operations/terraform-enterprise-wif), [TCP healthcheck and restart for unmanaged GCE instances](./cloud-operations/unmanaged-instances-healthcheck), [Migrate for Compute Engine (v5) blueprints](./cloud-operations/vm-migration), [Configuring workload identity federation to access Google Cloud resources from apps running on Azure](./cloud-operations/workload-identity-federation) - **data solutions** - [GCE and GCS CMEK via centralized Cloud KMS](./data-solutions/cmek-via-centralized-kms), [Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key](./data-solutions/composer-2), [Cloud SQL instance with multi-region read replicas](./data-solutions/cloudsql-multiregion), [Data Platform](./data-solutions/data-platform-foundations), [Spinning up a foundation data pipeline on Google Cloud using Cloud Storage, Dataflow and BigQuery](./data-solutions/gcs-to-bq-with-least-privileges), [#SQL Server Always On Groups blueprint](./data-solutions/sqlserver-alwayson), [Data Playground](./data-solutions/data-playground) -- **factories** - [[The why and the how of Resource Factories](./factories), [Google Cloud Identity Group Factory](./factories/cloud-identity-group-factory), [Google Cloud BQ Factory](./factories/bigquery-factory), [Google Cloud VPC Firewall Factory](./factories/net-vpc-firewall-yaml), [Minimal Project Factory](./factories/project-factory) +- **factories** - [The why and the how of Resource Factories](./factories), [Google Cloud Identity Group Factory](./factories/cloud-identity-group-factory), [Google Cloud BQ Factory](./factories/bigquery-factory), [Google Cloud VPC Firewall Factory](./factories/net-vpc-firewall-yaml), [Minimal Project Factory](./factories/project-factory) - **GKE** - [Binary Authorization Pipeline Blueprint](./gke/binauthz), [Storage API](./gke/binauthz/image), [Multi-cluster mesh on GKE (fleet API)](./gke/multi-cluster-mesh-gke-fleet-api), [GKE Multitenant Blueprint](./gke/multitenant-fleet), [Shared VPC with GKE support](./networking/shared-vpc-gke/) - **networking** - [Decentralized firewall management](./networking/decentralized-firewall), [Decentralized firewall validator](./networking/decentralized-firewall/validator), [Network filtering with Squid](./networking/filtering-proxy), [HTTP Load Balancer with Cloud Armor](./networking/glb-and-armor), [Hub and Spoke via VPN](./networking/hub-and-spoke-vpn), [Hub and Spoke via VPC Peering](./networking/hub-and-spoke-peering), [Internal Load Balancer as Next Hop](./networking/ilb-next-hop), [Nginx-based reverse proxy cluster](./networking/nginx-reverse-proxy-cluster), [On-prem DNS and Google Private Access](./networking/onprem-google-access-dns), [Calling a private Cloud Function from On-premises](./networking/private-cloud-function-from-onprem), [Hybrid connectivity to on-premise services through PSC](./networking/psc-hybrid), [PSC Producer](./networking/psc-hybrid/psc-producer), [PSC Consumer](./networking/psc-hybrid/psc-consumer), [Shared VPC with optional GKE cluster](./networking/shared-vpc-gke) - **serverless** - [Creating multi-region deployments for API Gateway](./serverless/api-gateway) diff --git a/blueprints/cloud-operations/network-dashboard/README.md b/blueprints/cloud-operations/network-dashboard/README.md index 15f06dae..c378723a 100644 --- a/blueprints/cloud-operations/network-dashboard/README.md +++ b/blueprints/cloud-operations/network-dashboard/README.md @@ -27,6 +27,8 @@ Clone this repository, then go through the following steps to create resources: - `terraform init` - `terraform apply` +Note: Org level viewing permission is required for some metrics such as firewall policies. + Once the resources are deployed, go to the following page to see the dashboard: https://console.cloud.google.com/monitoring/dashboards?project=. A dashboard called "quotas-utilization" should be created. @@ -75,6 +77,7 @@ In a future release, we could support: - Dynamic routes calculation for VPCs/PPGs with "global routing" set to OFF - Static routes calculation for projects/PPGs with "custom routes importing/exporting" set to OFF - Calculations for cross Organization peering groups +- Support different scopes (reduced and fine-grained) If you are interested in this and/or would like to contribute, please contact legranda@google.com. diff --git a/blueprints/cloud-operations/network-dashboard/main.tf b/blueprints/cloud-operations/network-dashboard/main.tf index 9d6122ca..5710f25c 100644 --- a/blueprints/cloud-operations/network-dashboard/main.tf +++ b/blueprints/cloud-operations/network-dashboard/main.tf @@ -50,7 +50,6 @@ module "service-account-function" { # Required IAM permissions for this service account are: # 1) compute.networkViewer on projects to be monitored (I gave it at organization level for now for simplicity) # 2) monitoring viewer on the projects to be monitored (I gave it at organization level for now for simplicity) - # 3) if you dont have permission to create service account and assign permission at organization Level, move these 3 roles to project level. iam_organization_roles = { "${var.organization_id}" = [ diff --git a/blueprints/cloud-operations/network-dashboard/versions.tf b/blueprints/cloud-operations/network-dashboard/versions.tf new file mode 100644 index 00000000..3bdf2337 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/versions.tf @@ -0,0 +1,27 @@ +# Copyright 2022 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 +# +# https://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. + +terraform { + required_version = ">= 1.3.1" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.40.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.40.0" # tftest + } + } +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md index 40e00f86..35198e8d 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md @@ -12,22 +12,22 @@ The codebase provisions the following list of resources: | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string | ✓ | | -| [project_id](variables.tf#L38) | Existing project id. | string | ✓ | | -| [tfe_organization_id](variables.tf#L43) | | | ✓ | | -| [tfe_workspace_id](variables.tf#L48) | | | ✓ | | -| [issuer_uri](variables.tf#L65) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | +| [project_id](variables.tf#L43) | Existing project id. | string | ✓ | | +| [tfe_organization_id](variables.tf#L48) | TFE organization id. | string | ✓ | | +| [tfe_workspace_id](variables.tf#L53) | TFE workspace id. | string | ✓ | | +| [issuer_uri](variables.tf#L21) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | | [parent](variables.tf#L27) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [project_create](variables.tf#L21) | Create project instead of using an existing one. | bool | | true | -| [workload_identity_pool_id](variables.tf#L53) | Workload identity pool id. | string | | "tfe-pool" | -| [workload_identity_pool_provider_id](variables.tf#L59) | Workload identity pool provider id. | string | | "tfe-provider" | +| [project_create](variables.tf#L37) | Create project instead of using an existing one. | bool | | true | +| [workload_identity_pool_id](variables.tf#L58) | Workload identity pool id. | string | | "tfe-pool" | +| [workload_identity_pool_provider_id](variables.tf#L64) | Workload identity pool provider id. | string | | "tfe-provider" | ## Outputs | name | description | sensitive | |---|---|:---:| -| [impersonate_service_account_email](outputs.tf#L31) | | | -| [project_id](outputs.tf#L16) | | | -| [workload_identity_audience](outputs.tf#L26) | | | -| [workload_identity_pool_provider_id](outputs.tf#L21) | GCP workload identity pool provider ID. | | +| [impersonate_service_account_email](outputs.tf#L16) | Service account to be impersonated by workload identity. | | +| [project_id](outputs.tf#L21) | GCP Project ID. | | +| [workload_identity_audience](outputs.tf#L26) | TFC Workload Identity Audience. | | +| [workload_identity_pool_provider_id](outputs.tf#L31) | GCP workload identity pool provider ID. | | diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf index 79cea39a..46d7f6b0 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf @@ -13,22 +13,22 @@ # limitations under the License. +output "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + value = module.sa-tfe.email +} + output "project_id" { description = "GCP Project ID." value = module.project.project_id } -output "workload_identity_pool_provider_id" { - description = "GCP workload identity pool provider ID." - value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name -} - output "workload_identity_audience" { description = "TFC Workload Identity Audience." value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.tfe-pool-provider.name}" } -output "impersonate_service_account_email" { - description = "Service account to be impersonated by workload identity." - value = module.sa-tfe.email +output "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name } diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf index 62163d17..3719b183 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf @@ -18,10 +18,10 @@ variable "billing_account" { type = string } -variable "project_create" { - description = "Create project instead of using an existing one." - type = bool - default = true +variable "issuer_uri" { + description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + type = string + default = "https://app.terraform.io/" } variable "parent" { @@ -34,6 +34,11 @@ variable "parent" { } } +variable "project_create" { + description = "Create project instead of using an existing one." + type = bool + default = true +} variable "project_id" { description = "Existing project id." @@ -61,9 +66,3 @@ variable "workload_identity_pool_provider_id" { type = string default = "tfe-provider" } - -variable "issuer_uri" { - description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." - type = string - default = "https://app.terraform.io/" -} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md index 5226dd64..9be8a09b 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md @@ -5,15 +5,14 @@ This terraform code is a part of [GCP Workload Identity Federation for Terraform The codebase provisions the following list of resources: - GCS Bucket - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [impersonate_service_account_email](variables.tf#L26) | | | ✓ | | -| [project_id](variables.tf#L16) | | | ✓ | | -| [workload_identity_pool_provider_id](variables.tf#L21) | GCP workload identity pool provider ID. | string | ✓ | | +| [impersonate_service_account_email](variables.tf#L21) | Service account to be impersonated by workload identity. | string | ✓ | | +| [project_id](variables.tf#L16) | GCP project ID. | string | ✓ | | +| [workload_identity_pool_provider_id](variables.tf#L26) | GCP workload identity pool provider ID. | string | ✓ | | diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf index 3f36c2ca..3a1d81dc 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf @@ -18,12 +18,12 @@ variable "project_id" { type = string } -variable "workload_identity_pool_provider_id" { - description = "GCP workload identity pool provider ID." - type = string -} - variable "impersonate_service_account_email" { description = "Service account to be impersonated by workload identity." type = string } + +variable "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + type = string +} diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index 7519fa8a..2990a2c5 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -67,8 +67,10 @@ module "orch-project" { "roles/storage.objectViewer" = [module.load-sa-df-0.iam_email] } oslogin = false - policy_boolean = { - "constraints/compute.requireOsLogin" = false + org_policies = { + "constraints/compute.requireOsLogin" = { + enforce = false + } } services = concat(var.project_services, [ "artifactregistry.googleapis.com", @@ -82,6 +84,7 @@ module "orch-project" { "container.googleapis.com", "containerregistry.googleapis.com", "dataflow.googleapis.com", + "orgpolicy.googleapis.com", "pubsub.googleapis.com", "servicenetworking.googleapis.com", "storage.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index 034bb32a..51545d58 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -160,9 +160,10 @@ You can find more details and best practices on using DLP to De-identification a [Data Catalog](https://cloud.google.com/data-catalog) helps you to document your data entry at scale. Data Catalog relies on [tags](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tags) and [tag template](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tag-templates) to manage metadata for all data entries in a unified and centralized service. To implement [column-level security](https://cloud.google.com/bigquery/docs/column-level-security-intro) on BigQuery, we suggest to use `Tags` and `Tag templates`. The default configuration will implement 3 tags: - - `3_Confidential`: policy tag for columns that include very sensitive information, such as credit card numbers. - - `2_Private`: policy tag for columns that include sensitive personal identifiable information (PII) information, such as a person's first name. - - `1_Sensitive`: policy tag for columns that include data that cannot be made public, such as the credit limit. + +- `3_Confidential`: policy tag for columns that include very sensitive information, such as credit card numbers. +- `2_Private`: policy tag for columns that include sensitive personal identifiable information (PII) information, such as a person's first name. +- `1_Sensitive`: policy tag for columns that include data that cannot be made public, such as the credit limit. Anything that is not tagged is available to all users who have access to the data warehouse. @@ -222,7 +223,7 @@ module "data-platform" { prefix = "myprefix" } -# tftest modules=42 resources=315 +# tftest modules=42 resources=316 ``` ## Customizations @@ -238,7 +239,7 @@ To do this, you need to remove IAM binging at project-level for the `data-analys ## Demo pipeline -The application layer is out of scope of this script. As a demo purpuse only, several Cloud Composer DAGs are provided. Demos will import data from the `drop off` area to the `Data Warehouse Confidential` dataset suing different features. +The application layer is out of scope of this script. As a demo purpuse only, several Cloud Composer DAGs are provided. Demos will import data from the `drop off` area to the `Data Warehouse Confidential` dataset suing different features. You can find examples in the `[demo](./demo)` folder. diff --git a/blueprints/data-solutions/data-playground/main.tf b/blueprints/data-solutions/data-playground/main.tf index 2bcd69ab..bcdea5df 100644 --- a/blueprints/data-solutions/data-playground/main.tf +++ b/blueprints/data-solutions/data-playground/main.tf @@ -35,13 +35,16 @@ module "project" { "dataflow.googleapis.com", "ml.googleapis.com", "notebooks.googleapis.com", + "orgpolicy.googleapis.com", "servicenetworking.googleapis.com", "stackdriver.googleapis.com", "storage.googleapis.com", "storage-component.googleapis.com" ] - policy_boolean = { - # "constraints/compute.requireOsLogin" = false + org_policies = { + # "constraints/compute.requireOsLogin" = { + # enforce = false + # } # Example of applying a project wide policy, mainly useful for Composer } service_encryption_key_ids = { diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index e496aa4d..cee829ff 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -68,13 +68,13 @@ module "projects" { iam = try(each.value.iam, {}) kms_service_agents = try(each.value.kms, {}) labels = try(each.value.labels, {}) - org_policies = try(each.value.org_policies, null) + org_policies = try(each.value.org_policies, {}) service_accounts = try(each.value.service_accounts, {}) services = try(each.value.services, []) service_identities_iam = try(each.value.service_identities_iam, {}) vpc = try(each.value.vpc, null) } -# tftest modules=7 resources=27 +# tftest modules=7 resources=29 ``` ### Projects configuration @@ -153,16 +153,16 @@ labels: environment: prod # [opt] Org policy overrides defined at project level -org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null +org_policies: + constraints/compute.disableGuestAttributesAccess: + enforce: true + constraints/compute.trustedImageProjects: + allow: values: - - projects/fast-prod-iac-core-0 + - projects/fast-dev-iac-core-0 + constraints/compute.vmExternalIpAccess: + deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format @@ -221,23 +221,28 @@ vpc: | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [project_id](variables.tf#L119) | Project id. | string | ✓ | | +| [project_id](variables.tf#L157) | Project id. | string | ✓ | | | [billing_alert](variables.tf#L22) | Billing alert configuration. | object({…}) | | null | | [defaults](variables.tf#L35) | Project factory default values. | object({…}) | | null | | [dns_zones](variables.tf#L57) | DNS private zones to create as child of var.defaults.environment_dns_zone. | list(string) | | [] | | [essential_contacts](variables.tf#L63) | Email contacts to be used for billing and GCP notifications. | list(string) | | [] | | [folder_id](variables.tf#L69) | Folder ID for the folder where the project will be created. | string | | null | | [group_iam](variables.tf#L75) | Custom IAM settings in group => [role] format. | map(list(string)) | | {} | -| [iam](variables.tf#L81) | Custom IAM settings in role => [principal] format. | map(list(string)) | | {} | -| [kms_service_agents](variables.tf#L87) | KMS IAM configuration in as service => [key]. | map(list(string)) | | {} | -| [labels](variables.tf#L93) | Labels to be assigned at project level. | map(string) | | {} | -| [org_policies](variables.tf#L99) | Org-policy overrides at project level. | object({…}) | | null | -| [prefix](variables.tf#L113) | Prefix used for the project id. | string | | null | -| [service_accounts](variables.tf#L124) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | -| [service_accounts_iam](variables.tf#L130) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]} | map(map(list(string))) | | {} | -| [service_identities_iam](variables.tf#L144) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [services](variables.tf#L137) | Services to be enabled for the project. | list(string) | | [] | -| [vpc](variables.tf#L151) | VPC configuration for the project. | object({…}) | | null | +| [group_iam_additive](variables.tf#L81) | Custom additive IAM settings in group => [role] format. | map(list(string)) | | {} | +| [iam](variables.tf#L87) | Custom IAM settings in role => [principal] format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L93) | Custom additive IAM settings in role => [principal] format. | map(list(string)) | | {} | +| [kms_service_agents](variables.tf#L99) | KMS IAM configuration in as service => [key]. | map(list(string)) | | {} | +| [labels](variables.tf#L105) | Labels to be assigned at project level. | map(string) | | {} | +| [org_policies](variables.tf#L111) | Org-policy overrides at project level. | map(object({…})) | | {} | +| [prefix](variables.tf#L151) | Prefix used for the project id. | string | | null | +| [service_accounts](variables.tf#L162) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | +| [service_accounts_additive](variables.tf#L168) | Service accounts to be created, and roles assigned them on the project additively. | map(list(string)) | | {} | +| [service_accounts_iam](variables.tf#L174) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]} | map(map(list(string))) | | {} | +| [service_accounts_iam_additive](variables.tf#L181) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]} | map(map(list(string))) | | {} | +| [service_identities_iam](variables.tf#L195) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [service_identities_iam_additive](variables.tf#L202) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [services](variables.tf#L188) | Services to be enabled for the project. | list(string) | | [] | +| [vpc](variables.tf#L209) | VPC configuration for the project. | object({…}) | | null | ## Outputs diff --git a/blueprints/factories/project-factory/main.tf b/blueprints/factories/project-factory/main.tf index 996b79e3..1fe5e1e4 100644 --- a/blueprints/factories/project-factory/main.tf +++ b/blueprints/factories/project-factory/main.tf @@ -21,7 +21,14 @@ locals { "group:${k}" if try(index(v, r), null) != null ] } - _group_iam_bindings = distinct(flatten(values(var.group_iam))) + _group_iam_additive = { + for r in local._group_iam_additive_bindings : r => [ + for k, v in var.group_iam_additive : + "group:${k}" if try(index(v, r), null) != null + ] + } + _group_iam_bindings = distinct(flatten(values(var.group_iam))) + _group_iam_additive_bindings = distinct(flatten(values(var.group_iam_additive))) _project_id = ( var.prefix == null || var.prefix == "" ? var.project_id @@ -37,9 +44,20 @@ locals { _service_accounts_iam_bindings = distinct(flatten( values(var.service_accounts) )) + _service_accounts_iam_additive = { + for r in local._service_accounts_iam_additive_bindings : r => [ + for k, v in var.service_accounts_additive : + module.service-accounts[k].iam_email + if try(index(v, r), null) != null + ] + } + _service_accounts_iam_additive_bindings = distinct(flatten( + values(var.service_accounts_additive) + )) _services = concat([ "billingbudgets.googleapis.com", - "essentialcontacts.googleapis.com" + "essentialcontacts.googleapis.com", + "orgpolicy.googleapis.com", ], length(var.dns_zones) > 0 ? ["dns.googleapis.com"] : [], try(var.vpc.gke_setup, null) != null ? ["container.googleapis.com"] : [], @@ -53,6 +71,14 @@ locals { if contains(roles, role) ] } + _service_identities_roles_additive = distinct(flatten(values(var.service_identities_iam_additive))) + _service_identities_iam_additive = { + for role in local._service_identities_roles_additive : role => [ + for service, roles in var.service_identities_iam_additive : + "serviceAccount:${module.project.service_accounts.robots[service]}" + if contains(roles, role) + ] + } _vpc_subnet_bindings = ( local.vpc.subnets_iam == null || local.vpc.host_project == null ? [] @@ -91,6 +117,20 @@ locals { try(local._service_identities_iam[role], []), ) } + iam_additive = { + for role in distinct(concat( + keys(var.iam_additive), + keys(local._group_iam_additive), + keys(local._service_accounts_iam_additive), + keys(local._service_identities_iam_additive), + )) : + role => concat( + try(var.iam_additive[role], []), + try(local._group_iam_additive[role], []), + try(local._service_accounts_iam_additive[role], []), + try(local._service_identities_iam_additive[role], []), + ) + } labels = merge( coalesce(var.labels, {}), coalesce(try(var.defaults.labels, {}), {}) ) @@ -147,10 +187,10 @@ module "project" { prefix = var.prefix contacts = { for c in local.essential_contacts : c => ["ALL"] } iam = local.iam + iam_additive = local.iam_additive labels = local.labels + org_policies = try(var.org_policies, {}) parent = var.folder_id - policy_boolean = try(var.org_policies.policy_boolean, {}) - policy_list = try(var.org_policies.policy_list, {}) service_encryption_key_ids = var.kms_service_agents services = local.services shared_vpc_service_config = var.vpc == null ? null : { diff --git a/blueprints/factories/project-factory/sample-data/projects/project.yaml b/blueprints/factories/project-factory/sample-data/projects/project.yaml index 13a8f5f5..88ba0bf5 100644 --- a/blueprints/factories/project-factory/sample-data/projects/project.yaml +++ b/blueprints/factories/project-factory/sample-data/projects/project.yaml @@ -48,15 +48,15 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null + constraints/compute.disableGuestAttributesAccess: + enforce: true + constraints/compute.trustedImageProjects: + allow: values: - projects/fast-dev-iac-core-0 + constraints/compute.vmExternalIpAccess: + deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index 6154c032..8efc0bc1 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/variables.tf @@ -78,12 +78,24 @@ variable "group_iam" { default = {} } +variable "group_iam_additive" { + description = "Custom additive IAM settings in group => [role] format." + type = map(list(string)) + default = {} +} + variable "iam" { description = "Custom IAM settings in role => [principal] format." type = map(list(string)) default = {} } +variable "iam_additive" { + description = "Custom additive IAM settings in role => [principal] format." + type = map(list(string)) + default = {} +} + variable "kms_service_agents" { description = "KMS IAM configuration in as service => [key]." type = map(list(string)) @@ -98,16 +110,42 @@ variable "labels" { variable "org_policies" { description = "Org-policy overrides at project level." - type = object({ - policy_boolean = map(bool) - policy_list = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) })) - }) - default = null + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + 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, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false } variable "prefix" { @@ -127,6 +165,12 @@ variable "service_accounts" { default = {} } +variable "service_accounts_additive" { + description = "Service accounts to be created, and roles assigned them on the project additively." + type = map(list(string)) + default = {} +} + variable "service_accounts_iam" { description = "IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}" type = map(map(list(string))) @@ -134,6 +178,13 @@ variable "service_accounts_iam" { nullable = false } +variable "service_accounts_iam_additive" { + description = "IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}" + type = map(map(list(string))) + default = {} + nullable = false +} + variable "services" { description = "Services to be enabled for the project." type = list(string) @@ -148,6 +199,13 @@ variable "service_identities_iam" { nullable = false } +variable "service_identities_iam_additive" { + description = "Custom additive IAM settings for service identities in service => [role] format." + type = map(list(string)) + default = {} + nullable = false +} + variable "vpc" { description = "VPC configuration for the project." type = object({ @@ -160,6 +218,3 @@ variable "vpc" { }) default = null } - - - diff --git a/blueprints/networking/filtering-proxy/main.tf b/blueprints/networking/filtering-proxy/main.tf index 884fbd30..ca998bf9 100644 --- a/blueprints/networking/filtering-proxy/main.tf +++ b/blueprints/networking/filtering-proxy/main.tf @@ -165,33 +165,31 @@ module "squid-vm" { } module "squid-mig" { - count = var.mig ? 1 : 0 - source = "../../../modules/compute-mig" - project_id = module.project-host.project_id - location = "${var.region}-b" - name = "squid-mig" - target_size = 1 - autoscaler_config = { - max_replicas = 10 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null + count = var.mig ? 1 : 0 + source = "../../../modules/compute-mig" + project_id = module.project-host.project_id + location = "${var.region}-b" + name = "squid-mig" + instance_template = module.squid-vm.template.self_link + target_size = 1 + auto_healing_policies = { + initial_delay_sec = 60 } - default_version = { - instance_template = module.squid-vm.template.self_link - name = "default" + autoscaler_config = { + max_replicas = 10 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } health_check_config = { - type = "tcp" - check = { port = 3128 } - config = {} - logging = true - } - auto_healing_policies = { - health_check = module.squid-mig.0.health_check.self_link - initial_delay_sec = 60 + enable_logging = true + tcp = { + port = 3128 + } } } @@ -226,13 +224,10 @@ module "folder-apps" { source = "../../../modules/folder" parent = var.root_node name = "apps" - policy_list = { + org_policies = { # prevent VMs with public IPs in the apps folder "constraints/compute.vmExternalIpAccess" = { - inherit_from_parent = false - suggested_value = null - status = false - values = [] + deny = { all = true } } } } diff --git a/blueprints/networking/glb-and-armor/main.tf b/blueprints/networking/glb-and-armor/main.tf index 6e43bc15..83622609 100644 --- a/blueprints/networking/glb-and-armor/main.tf +++ b/blueprints/networking/glb-and-armor/main.tf @@ -153,22 +153,20 @@ module "vm_siege" { } module "mig_ew1" { - source = "../../../modules/compute-mig" - project_id = module.project.project_id - location = "europe-west1" - name = "${local.prefix}europe-west1-mig" - regional = true - default_version = { - instance_template = module.instance_template_ew1.template.self_link - name = "default" - } + source = "../../../modules/compute-mig" + project_id = module.project.project_id + location = "europe-west1" + name = "${local.prefix}europe-west1-mig" + instance_template = module.instance_template_ew1.template.self_link autoscaler_config = { - max_replicas = 5 - min_replicas = 1 - cooldown_period = 45 - cpu_utilization_target = 0.8 - load_balancing_utilization_target = null - metric = null + max_replicas = 5 + min_replicas = 1 + cooldown_period = 45 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } named_ports = { http = 80 @@ -179,22 +177,20 @@ module "mig_ew1" { } module "mig_ue1" { - source = "../../../modules/compute-mig" - project_id = module.project.project_id - location = "us-east1" - name = "${local.prefix}us-east1-mig" - regional = true - default_version = { - instance_template = module.instance_template_ue1.template.self_link - name = "default" - } + source = "../../../modules/compute-mig" + project_id = module.project.project_id + location = "us-east1" + name = "${local.prefix}us-east1-mig" + instance_template = module.instance_template_ue1.template.self_link autoscaler_config = { - max_replicas = 5 - min_replicas = 1 - cooldown_period = 45 - cpu_utilization_target = 0.8 - load_balancing_utilization_target = null - metric = null + max_replicas = 5 + min_replicas = 1 + cooldown_period = 45 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } named_ports = { http = 80 diff --git a/fast/stages/00-bootstrap/automation.tf b/fast/stages/00-bootstrap/automation.tf index cf48d9cd..13eb68f1 100644 --- a/fast/stages/00-bootstrap/automation.tf +++ b/fast/stages/00-bootstrap/automation.tf @@ -72,6 +72,7 @@ module "automation-project" { "essentialcontacts.googleapis.com", "iam.googleapis.com", "iamcredentials.googleapis.com", + "orgpolicy.googleapis.com", "pubsub.googleapis.com", "servicenetworking.googleapis.com", "serviceusage.googleapis.com", diff --git a/fast/stages/01-resman/branch-sandbox.tf b/fast/stages/01-resman/branch-sandbox.tf index 7ed154aa..84995c15 100644 --- a/fast/stages/01-resman/branch-sandbox.tf +++ b/fast/stages/01-resman/branch-sandbox.tf @@ -32,16 +32,9 @@ module "branch-sandbox-folder" { "roles/resourcemanager.folderAdmin" = [module.branch-sandbox-sa.0.iam_email] "roles/resourcemanager.projectCreator" = [module.branch-sandbox-sa.0.iam_email] } - policy_boolean = { - "constraints/sql.restrictPublicIp" = false - } - policy_list = { - "constraints/compute.vmExternalIpAccess" = { - inherit_from_parent = false - suggested_value = null - status = true - values = [] - } + org_policies = { + "constraints/sql.restrictPublicIp" = { enforce = false } + "constraints/compute.vmExternalIpAccess" = { allow = { all = true } } } tag_bindings = { context = try( diff --git a/fast/stages/01-resman/organization.tf b/fast/stages/01-resman/organization.tf index 6596f9c0..40a789ee 100644 --- a/fast/stages/01-resman/organization.tf +++ b/fast/stages/01-resman/organization.tf @@ -18,18 +18,11 @@ locals { - list_allow = { - inherit_from_parent = false - suggested_value = null - status = true - values = [] - } - list_deny = { - inherit_from_parent = false - suggested_value = null - status = false - values = [] - } + all_drs_domains = concat( + [var.organization.customer_id], + try(local.policy_configs.allowed_policy_member_domains, []) + ) + policy_configs = ( var.organization_policy_configs == null ? {} @@ -74,74 +67,54 @@ module "organization" { } : {} ) # sample subset of useful organization policies, edit to suit requirements - policy_boolean = { - # "constraints/cloudfunctions.requireVPCConnector" = true - # "constraints/compute.disableGuestAttributesAccess" = true - # "constraints/compute.disableInternetNetworkEndpointGroup" = true - # "constraints/compute.disableNestedVirtualization" = true - # "constraints/compute.disableSerialPortAccess" = true - "constraints/compute.requireOsLogin" = true - # "constraints/compute.restrictXpnProjectLienRemoval" = true - "constraints/compute.skipDefaultNetworkCreation" = true - # "constraints/compute.setNewProjectDefaultToZonalDNSOnly" = true - "constraints/iam.automaticIamGrantsForDefaultServiceAccounts" = true - "constraints/iam.disableServiceAccountKeyCreation" = true - # "constraints/iam.disableServiceAccountKeyUpload" = true - "constraints/sql.restrictPublicIp" = true - "constraints/sql.restrictAuthorizedNetworks" = true - "constraints/storage.uniformBucketLevelAccess" = true - } - policy_list = { - # "constraints/cloudfunctions.allowedIngressSettings" = merge( - # local.list_allow, { values = ["is:ALLOW_INTERNAL_ONLY"] } - # ) - # "constraints/cloudfunctions.allowedVpcConnectorEgressSettings" = merge( - # local.list_allow, { values = ["is:PRIVATE_RANGES_ONLY"] } - # ) - "constraints/compute.restrictLoadBalancerCreationForTypes" = merge( - local.list_allow, { values = ["in:INTERNAL"] } - ) - "constraints/compute.vmExternalIpAccess" = local.list_deny - "constraints/iam.allowedPolicyMemberDomains" = merge( - local.list_allow, { - values = concat( - [var.organization.customer_id], - try(local.policy_configs.allowed_policy_member_domains, []) - ) - }) - "constraints/run.allowedIngress" = merge( - local.list_allow, { values = ["is:internal"] } - ) - # "constraints/run.allowedVPCEgress" = merge( - # local.list_allow, { values = ["is:private-ranges-only"] } - # ) - # "constraints/compute.restrictCloudNATUsage" = local.list_deny - # "constraints/compute.restrictDedicatedInterconnectUsage" = local.list_deny - # "constraints/compute.restrictPartnerInterconnectUsage" = local.list_deny - # "constraints/compute.restrictProtocolForwardingCreationForTypes" = local.list_deny - # "constraints/compute.restrictSharedVpcHostProjects" = local.list_deny - # "constraints/compute.restrictSharedVpcSubnetworks" = local.list_deny - # "constraints/compute.restrictVpcPeering" = local.list_deny - # "constraints/compute.restrictVpnPeerIPs" = local.list_deny - # "constraints/compute.vmCanIpForward" = local.list_deny - # "constraints/gcp.resourceLocations" = { - # inherit_from_parent = false - # suggested_value = null - # status = true - # values = local.allowed_regions + + org_policies = { + "compute.disableGuestAttributesAccess" = { enforce = true } + "compute.requireOsLogin" = { enforce = true } + "compute.restrictLoadBalancerCreationForTypes" = { allow = { values = ["in:INTERNAL"] } } + "compute.skipDefaultNetworkCreation" = { enforce = true } + "compute.vmExternalIpAccess" = { deny = { all = true } } + "iam.allowedPolicyMemberDomains" = { allow = { values = local.all_drs_domains } } + "iam.automaticIamGrantsForDefaultServiceAccounts" = { enforce = true } + "iam.disableServiceAccountKeyCreation" = { enforce = true } + "iam.disableServiceAccountKeyUpload" = { enforce = true } + "run.allowedIngress" = { allow = { values = ["is:internal"] } } + "sql.restrictAuthorizedNetworks" = { enforce = true } + "sql.restrictPublicIp" = { enforce = true } + "storage.uniformBucketLevelAccess" = { enforce = true } + # "cloudfunctions.allowedIngressSettings" = { + # allow = { values = ["is:ALLOW_INTERNAL_ONLY"] } # } - # https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers#restrict - # "constraints/iam.workloadIdentityPoolProviders" = merge( - # local.list_allow, { values = [ - # for k, v in coalesce(var.automation.federated_identity_providers, {}) : - # v.issuer_uri - # ] } - # ) - # "constraints/iam.workloadIdentityPoolAwsAccounts" = merge( - # local.list_allow, { values = [ - # - # ] } - # ) + # "cloudfunctions.allowedVpcConnectorEgressSettings" = { + # allow = { values = ["is:PRIVATE_RANGES_ONLY"] } + # } + # "cloudfunctions.requireVPCConnector" = { enforce = true } + # "compute.disableInternetNetworkEndpointGroup" = { enforce = true } + # "compute.disableNestedVirtualization" = { enforce = true } + # "compute.disableSerialPortAccess" = { enforce = true } + # "compute.restrictCloudNATUsage" = { deny = { all = true }} + # "compute.restrictDedicatedInterconnectUsage" = { deny = { all = true }} + # "compute.restrictPartnerInterconnectUsage" = { deny = { all = true }} + # "compute.restrictProtocolForwardingCreationForTypes" = { deny = { all = true }} + # "compute.restrictSharedVpcHostProjects" = { deny = { all = true }} + # "compute.restrictSharedVpcSubnetworks" = { deny = { all = true }} + # "compute.restrictVpcPeering" = { deny = { all = true }} + # "compute.restrictVpnPeerIPs" = { deny = { all = true }} + # "compute.restrictXpnProjectLienRemoval" = { enforce = true } + # "compute.setNewProjectDefaultToZonalDNSOnly" = { enforce = true } + # "compute.vmCanIpForward" = { deny = { all = true }} + # "gcp.resourceLocations" = { + # allow = { values = local.allowed_regions } + # } + # "iam.workloadIdentityPoolProviders" = { + # allow = { + # values = [ + # for k, v in coalesce(var.automation.federated_identity_providers, {}) : + # v.issuer_uri + # ] + # } + # } + # "run.allowedVPCEgress" = { allow = { values = ["is:private-ranges-only"] } } } tags = { (var.tag_names.context) = { diff --git a/fast/stages/02-networking-nva/nva.tf b/fast/stages/02-networking-nva/nva.tf index 4e70d02f..d0afbd72 100644 --- a/fast/stages/02-networking-nva/nva.tf +++ b/fast/stages/02-networking-nva/nva.tf @@ -15,7 +15,7 @@ */ locals { - # routing_config should be aligned to the NVA network interfaces - i.e. + # routing_config should be aligned to the NVA network interfaces - i.e. # local.routing_config[0] sets up the first interface, and so on. routing_config = [ { @@ -94,27 +94,21 @@ module "nva-template" { } module "nva-mig" { - for_each = local.nva_locality - source = "../../../modules/compute-mig" - project_id = module.landing-project.project_id - regional = true - location = each.value.region - name = "nva-cos-${each.value.trigram}-${each.value.zone}" - target_size = 1 - # FIXME: cycle - # auto_healing_policies = { - # health_check = module.nva-mig[each.key].health_check.self_link - # initial_delay_sec = 30 - # } - health_check_config = { - type = "tcp" - check = { port = 22 } - config = {} - logging = true + for_each = local.nva_locality + source = "../../../modules/compute-mig" + project_id = module.landing-project.project_id + location = each.value.region + name = "nva-cos-${each.value.trigram}-${each.value.zone}" + instance_template = module.nva-template[each.key].template.self_link + target_size = 1 + auto_healing_policies = { + initial_delay_sec = 30 } - default_version = { - instance_template = module.nva-template[each.key].template.self_link - name = "default" + health_check_config = { + enable_logging = true + tcp = { + port = 22 + } } } diff --git a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample b/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample index 13a8f5f5..88ba0bf5 100644 --- a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample +++ b/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample @@ -48,15 +48,15 @@ labels: # [opt] Org policy overrides defined at project level org_policies: - policy_boolean: - constraints/compute.disableGuestAttributesAccess: true - policy_list: - constraints/compute.trustedImageProjects: - inherit_from_parent: null - status: true - suggested_value: null + constraints/compute.disableGuestAttributesAccess: + enforce: true + constraints/compute.trustedImageProjects: + allow: values: - projects/fast-dev-iac-core-0 + constraints/compute.vmExternalIpAccess: + deny: + all: true # [opt] Service account to create for the project and their roles on the project # in name => [roles] format diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md index 1d421527..4e5b5a45 100644 --- a/modules/compute-mig/README.md +++ b/modules/compute-mig/README.md @@ -2,7 +2,7 @@ This module allows creating a managed instance group supporting one or more application versions via instance templates. Optionally, a health check and an autoscaler can be created, and the managed instance group can be configured to be stateful. -This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below. +This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below. Stateful disks can be created directly, as shown in the last example below. @@ -39,15 +39,12 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 2 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 2 + instance_template = module.nginx-template.template.self_link } # tftest modules=2 resources=2 ``` @@ -85,20 +82,18 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link versions = { canary = { instance_template = module.nginx-template.template.self_link - target_type = "fixed" - target_size = 1 + target_size = { + fixed = 1 + } } } } @@ -138,24 +133,20 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link auto_healing_policies = { - health_check = module.nginx-mig.health_check.self_link initial_delay_sec = 30 } health_check_config = { - type = "http" - check = { port = 80 } - config = {} - logging = true + enable_logging = true + http = { + port = 80 + } } } # tftest modules=2 resources=3 @@ -194,22 +185,21 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link autoscaler_config = { - max_replicas = 3 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null + max_replicas = 3 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } } # tftest modules=2 resources=3 @@ -246,23 +236,19 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link update_policy = { - type = "PROACTIVE" minimal_action = "REPLACE" + type = "PROACTIVE" min_ready_sec = 30 - max_surge_type = "fixed" - max_surge = 1 - max_unavailable_type = null - max_unavailable = null + max_surge = { + fixed = 1 + } } } # tftest modules=2 resources=2 @@ -270,7 +256,7 @@ module "nginx-mig" { ### Stateful MIGs - MIG Config -Stateful MIGs have some limitations documented [here](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-migs#limitations). Enforcement of these requirements is the responsibility of users of this module. +Stateful MIGs have some limitations documented [here](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-migs#limitations). Enforcement of these requirements is the responsibility of users of this module. You can configure a disk defined in the instance template to be stateful for all instances in the MIG by configuring in the MIG's stateful policy, using the `stateful_disk_mig` variable. Alternatively, you can also configure stateful persistent disks individually per instance of the MIG by setting the `stateful_disk_instance` variable. A discussion on these scenarios can be found in the [docs](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-disks-in-migs). @@ -278,7 +264,6 @@ An example using only the configuration at the MIG level can be seen below. Note that when referencing the stateful disk, you use `device_name` and not `disk_name`. - ```hcl module "cos-nginx" { source = "./fabric/modules/cloud-config-container/nginx" @@ -319,40 +304,33 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link autoscaler_config = { - max_replicas = 3 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null - } - stateful_config = { - per_instance_config = {}, - mig_config = { - stateful_disks = { - repd-1 = { - delete_rule = "NEVER" - } + max_replicas = 3 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 } } } + stateful_disks = { + repd-1 = null + } } # tftest modules=2 resources=3 ``` ### Stateful MIGs - Instance Config -Here is an example defining the stateful config at the instance level. + +Here is an example defining the stateful config at the instance level. Note that you will need to know the instance name in order to use this configuration. @@ -396,46 +374,36 @@ module "nginx-template" { } module "nginx-mig" { - source = "./fabric/modules/compute-mig" - project_id = "my-project" - location = "europe-west1-b" - name = "mig-test" - target_size = 3 - default_version = { - instance_template = module.nginx-template.template.self_link - name = "default" - } + source = "./fabric/modules/compute-mig" + project_id = "my-project" + location = "europe-west1-b" + name = "mig-test" + target_size = 3 + instance_template = module.nginx-template.template.self_link autoscaler_config = { - max_replicas = 3 - min_replicas = 1 - cooldown_period = 30 - cpu_utilization_target = 0.65 - load_balancing_utilization_target = null - metric = null + max_replicas = 3 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.65 + } + } } stateful_config = { - per_instance_config = { - # note that this needs to be the name of an existing instance within the Managed Instance Group - instance-1 = { - stateful_disks = { + # name needs to match a MIG instance name + instance-1 = { + minimal_action = "NONE", + most_disruptive_allowed_action = "REPLACE" + preserved_state = { + disks = { persistent-disk-1 = { source = "test-disk", - mode = "READ_ONLY", - delete_rule= "NEVER", - }, - }, + } + } metadata = { foo = "bar" - }, - update_config = { - minimal_action = "NONE", - most_disruptive_allowed_action = "REPLACE", - remove_instance_state_on_destroy = false, - }, - }, - }, - mig_config = { - stateful_disks = { + } } } } @@ -449,21 +417,25 @@ module "nginx-mig" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [default_version](variables.tf#L45) | Default application version template. Additional versions can be specified via the `versions` variable. | object({…}) | ✓ | | -| [location](variables.tf#L64) | Compute zone, or region if `regional` is set to true. | string | ✓ | | -| [name](variables.tf#L68) | Managed group name. | string | ✓ | | -| [project_id](variables.tf#L79) | Project id. | string | ✓ | | -| [auto_healing_policies](variables.tf#L17) | Auto-healing policies for this group. | object({…}) | | null | -| [autoscaler_config](variables.tf#L26) | Optional autoscaler configuration. Only one of 'cpu_utilization_target' 'load_balancing_utilization_target' or 'metric' can be not null. | object({…}) | | null | -| [health_check_config](variables.tf#L53) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | null | -| [named_ports](variables.tf#L73) | Named ports. | map(number) | | null | -| [regional](variables.tf#L84) | Use regional instance group. When set, `location` should be set to the region. | bool | | false | -| [stateful_config](variables.tf#L90) | Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name. | object({…}) | | null | -| [target_pools](variables.tf#L121) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | -| [target_size](variables.tf#L127) | Group target size, leave null when using an autoscaler. | number | | null | -| [update_policy](variables.tf#L133) | Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'. | object({…}) | | null | -| [versions](variables.tf#L148) | Additional application versions, target_type is either 'fixed' or 'percent'. | map(object({…})) | | null | -| [wait_for_instances](variables.tf#L158) | Wait for all instances to be created/updated before returning. | bool | | null | +| [instance_template](variables.tf#L150) | Instance template for the default version. | string | ✓ | | +| [location](variables.tf#L155) | Compute zone or region. | string | ✓ | | +| [name](variables.tf#L160) | Managed group name. | string | ✓ | | +| [project_id](variables.tf#L171) | Project id. | string | ✓ | | +| [all_instances_config](variables.tf#L17) | Metadata and labels set to all instances in the group. | object({…}) | | null | +| [auto_healing_policies](variables.tf#L26) | Auto-healing policies for this group. | object({…}) | | null | +| [autoscaler_config](variables.tf#L35) | Optional autoscaler configuration. | object({…}) | | null | +| [default_version_name](variables.tf#L83) | Name used for the default version. | string | | "default" | +| [description](variables.tf#L89) | Optional description used for all resources managed by this module. | string | | "Terraform managed." | +| [distribution_policy](variables.tf#L95) | DIstribution policy for regional MIG. | object({…}) | | null | +| [health_check_config](variables.tf#L104) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | null | +| [named_ports](variables.tf#L165) | Named ports. | map(number) | | null | +| [stateful_config](variables.tf#L183) | Stateful configuration for individual instances. | map(object({…})) | | {} | +| [stateful_disks](variables.tf#L176) | Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean. | map(bool) | | {} | +| [target_pools](variables.tf#L202) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | +| [target_size](variables.tf#L208) | Group target size, leave null when using an autoscaler. | number | | null | +| [update_policy](variables.tf#L214) | Update policy. Minimal action and type are required. | object({…}) | | null | +| [versions](variables.tf#L235) | Additional application versions, target_size is optional. | map(object({…})) | | {} | +| [wait_for_instances](variables.tf#L248) | Wait for all instances to be created/updated before returning. | object({…}) | | null | ## Outputs diff --git a/modules/compute-mig/autoscaler.tf b/modules/compute-mig/autoscaler.tf new file mode 100644 index 00000000..b8bd0acc --- /dev/null +++ b/modules/compute-mig/autoscaler.tf @@ -0,0 +1,229 @@ +/** + * Copyright 2022 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 Autoscaler resource. + +locals { + as_enabled = true + as_scaling = try(var.autoscaler_config.scaling_control, null) + as_signals = try(var.autoscaler_config.scaling_signals, null) +} + +resource "google_compute_autoscaler" "default" { + provider = google-beta + count = local.is_regional || var.autoscaler_config == null ? 0 : 1 + project = var.project_id + name = var.name + zone = var.location + description = var.description + target = google_compute_instance_group_manager.default.0.id + + autoscaling_policy { + max_replicas = var.autoscaler_config.max_replicas + min_replicas = var.autoscaler_config.min_replicas + cooldown_period = var.autoscaler_config.cooldown_period + + dynamic "scale_down_control" { + for_each = local.as_scaling.down == null ? [] : [""] + content { + time_window_sec = local.as_scaling.down.time_window_sec + dynamic "max_scaled_down_replicas" { + for_each = ( + local.as_scaling.down.max_replicas_fixed == null && + local.as_scaling.down.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.down.max_replicas_fixed + percent = local.as_scaling.down.max_replicas_percent + } + } + } + } + + dynamic "scale_in_control" { + for_each = local.as_scaling.in == null ? [] : [""] + content { + time_window_sec = local.as_scaling.in.time_window_sec + dynamic "max_scaled_in_replicas" { + for_each = ( + local.as_scaling.in.max_replicas_fixed == null && + local.as_scaling.in.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.in.max_replicas_fixed + percent = local.as_scaling.in.max_replicas_percent + } + } + } + } + + dynamic "cpu_utilization" { + for_each = local.as_signals.cpu_utilization == null ? [] : [""] + content { + target = local.as_signals.cpu_utilization.target + predictive_method = ( + local.as_signals.cpu_utilization.optimize_availability == true + ? "OPTIMIZE_AVAILABILITY" + : null + ) + } + } + + dynamic "load_balancing_utilization" { + for_each = local.as_signals.load_balancing_utilization == null ? [] : [""] + content { + target = local.as_signals.load_balancing_utilization.target + } + } + + dynamic "metric" { + for_each = toset( + local.as_signals.metrics == null ? [] : local.as_signals.metrics + ) + content { + name = metric.value.name + type = metric.value.type + target = metric.value.target_value + single_instance_assignment = metric.value.single_instance_assignment + filter = metric.value.time_series_filter + } + } + + dynamic "scaling_schedules" { + for_each = toset( + local.as_signals.schedules == null ? [] : local.as_signals.schedules + ) + iterator = schedule + content { + duration_sec = schedule.value.duration_sec + min_required_replicas = schedule.value.min_required_replicas + name = schedule.value.name + schedule = schedule.value.cron_schedule + description = schedule.value.description + disabled = schedule.value.disabled + time_zone = schedule.value.timezone + } + } + + } +} + +resource "google_compute_region_autoscaler" "default" { + provider = google-beta + count = local.is_regional && var.autoscaler_config != null ? 1 : 0 + project = var.project_id + name = var.name + region = var.location + description = var.description + target = google_compute_region_instance_group_manager.default.0.id + + autoscaling_policy { + max_replicas = var.autoscaler_config.max_replicas + min_replicas = var.autoscaler_config.min_replicas + cooldown_period = var.autoscaler_config.cooldown_period + + dynamic "scale_down_control" { + for_each = local.as_scaling.down == null ? [] : [""] + content { + time_window_sec = local.as_scaling.down.time_window_sec + dynamic "max_scaled_down_replicas" { + for_each = ( + local.as_scaling.down.max_replicas_fixed == null && + local.as_scaling.down.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.down.max_replicas_fixed + percent = local.as_scaling.down.max_replicas_percent + } + } + } + } + + dynamic "scale_in_control" { + for_each = local.as_scaling.in == null ? [] : [""] + content { + time_window_sec = local.as_scaling.in.time_window_sec + dynamic "max_scaled_in_replicas" { + for_each = ( + local.as_scaling.in.max_replicas_fixed == null && + local.as_scaling.in.max_replicas_percent == null + ? [] + : [""] + ) + content { + fixed = local.as_scaling.in.max_replicas_fixed + percent = local.as_scaling.in.max_replicas_percent + } + } + } + } + + dynamic "cpu_utilization" { + for_each = local.as_signals.cpu_utilization == null ? [] : [""] + content { + target = local.as_signals.cpu_utilization.target + predictive_method = ( + local.as_signals.cpu_utilization.optimize_availability == true + ? "OPTIMIZE_AVAILABILITY" + : null + ) + } + } + + dynamic "load_balancing_utilization" { + for_each = local.as_signals.load_balancing_utilization == null ? [] : [""] + content { + target = local.as_signals.load_balancing_utilization.target + } + } + + dynamic "metric" { + for_each = toset( + local.as_signals.metrics == null ? [] : local.as_signals.metrics + ) + content { + name = metric.value.name + type = metric.value.type + target = metric.value.target_value + single_instance_assignment = metric.value.single_instance_assignment + filter = metric.value.time_series_filter + } + } + + dynamic "scaling_schedules" { + for_each = toset( + local.as_signals.schedules == null ? [] : local.as_signals.schedules + ) + iterator = schedule + content { + duration_sec = schedule.value.duration_sec + min_required_replicas = schedule.value.min_required_replicas + name = schedule.value.name + schedule = schedule.cron_schedule + description = schedule.value.description + disabled = schedule.value.disabled + time_zone = schedule.value.timezone + } + } + + } +} diff --git a/modules/compute-mig/health-check.tf b/modules/compute-mig/health-check.tf new file mode 100644 index 00000000..9a5d8d1f --- /dev/null +++ b/modules/compute-mig/health-check.tf @@ -0,0 +1,128 @@ +/** + * Copyright 2022 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 Health check resource. + +locals { + hc = var.health_check_config + hc_grpc = try(local.hc.grpc, null) != null + hc_http = ( + try(local.hc.http, null) != null && + lower(try(local.hc.http.use_protocol, "")) == "http" + ) + hc_http2 = ( + try(local.hc.http, null) != null && + lower(try(local.hc.http.use_protocol, "")) == "http2" + ) + hc_https = ( + try(local.hc.http, null) != null && + lower(try(local.hc.http.use_protocol, "")) == "https" + ) + hc_ssl = try(local.hc.tcp.use_ssl, null) == true + hc_tcp = try(local.hc.tcp, null) != null && !local.hc_ssl +} + +resource "google_compute_health_check" "autohealing" { + provider = google-beta + count = local.hc != null ? 1 : 0 + project = var.project_id + name = var.name + description = local.hc.description + check_interval_sec = local.hc.check_interval_sec + healthy_threshold = local.hc.healthy_threshold + timeout_sec = local.hc.timeout_sec + unhealthy_threshold = local.hc.unhealthy_threshold + + dynamic "grpc_health_check" { + for_each = local.hc_grpc ? [""] : [] + content { + port = local.hc.grpc.port + port_name = local.hc.grpc.port_name + port_specification = local.hc.grpc.port_specification + grpc_service_name = local.hc.grpc.service_name + } + } + + dynamic "http_health_check" { + for_each = local.hc_http ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "http2_health_check" { + for_each = local.hc_http2 ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "https_health_check" { + for_each = local.hc_https ? [""] : [] + content { + host = local.hc.http.host + port = local.hc.http.port + port_name = local.hc.http.port_name + port_specification = local.hc.http.port_specification + proxy_header = local.hc.http.proxy_header + request_path = local.hc.http.request_path + response = local.hc.http.response + } + } + + dynamic "ssl_health_check" { + for_each = local.hc_ssl ? [""] : [] + content { + port = local.hc.tcp.port + port_name = local.hc.tcp.port_name + port_specification = local.hc.tcp.port_specification + proxy_header = local.hc.tcp.proxy_header + request = local.hc.tcp.request + response = local.hc.tcp.response + } + } + + dynamic "tcp_health_check" { + for_each = local.hc_tcp ? [""] : [] + content { + port = local.hc.tcp.port + port_name = local.hc.tcp.port_name + port_specification = local.hc.tcp.port_specification + proxy_header = local.hc.tcp.proxy_header + request = local.hc.tcp.request + response = local.hc.tcp.response + } + } + + dynamic "log_config" { + for_each = try(local.hc.enable_logging, null) == true ? [""] : [] + content { + enable = true + } + } +} diff --git a/modules/compute-mig/main.tf b/modules/compute-mig/main.tf index b5a71fac..92738995 100644 --- a/modules/compute-mig/main.tf +++ b/modules/compute-mig/main.tf @@ -14,105 +14,50 @@ * limitations under the License. */ -resource "google_compute_autoscaler" "default" { - provider = google-beta - count = var.regional || var.autoscaler_config == null ? 0 : 1 - project = var.project_id - name = var.name - description = "Terraform managed." - zone = var.location - target = google_compute_instance_group_manager.default.0.id - - autoscaling_policy { - max_replicas = var.autoscaler_config.max_replicas - min_replicas = var.autoscaler_config.min_replicas - cooldown_period = var.autoscaler_config.cooldown_period - - dynamic "cpu_utilization" { - for_each = ( - var.autoscaler_config.cpu_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.cpu_utilization_target - } - } - - dynamic "load_balancing_utilization" { - for_each = ( - var.autoscaler_config.load_balancing_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.load_balancing_utilization_target - } - } - - dynamic "metric" { - for_each = ( - var.autoscaler_config.metric == null - ? [] - : [var.autoscaler_config.metric] - ) - iterator = config - content { - name = config.value.name - single_instance_assignment = config.value.single_instance_assignment - target = config.value.target - type = config.value.type - filter = config.value.filter - } - } - } +locals { + health_check = ( + try(var.auto_healing_policies.health_check, null) == null + ? try(google_compute_health_check.autohealing.0.self_link, null) + : try(var.auto_healing_policies.health_check, null) + ) + instance_group_manager = ( + local.is_regional ? + google_compute_region_instance_group_manager.default : + google_compute_instance_group_manager.default + ) + is_regional = length(split("-", var.location)) == 2 } - resource "google_compute_instance_group_manager" "default" { - provider = google-beta - count = var.regional ? 0 : 1 - project = var.project_id - zone = var.location - name = var.name - base_instance_name = var.name - description = "Terraform-managed." - target_size = var.target_size - target_pools = var.target_pools - wait_for_instances = var.wait_for_instances + provider = google-beta + count = local.is_regional ? 0 : 1 + project = var.project_id + zone = var.location + name = var.name + base_instance_name = var.name + description = var.description + target_size = var.target_size + target_pools = var.target_pools + wait_for_instances = try(var.wait_for_instances.enabled, null) + wait_for_instances_status = try(var.wait_for_instances.status, null) + + dynamic "all_instances_config" { + for_each = var.all_instances_config == null ? [] : [""] + content { + labels = try(var.all_instances_config.labels, null) + metadata = try(var.all_instances_config.metadata, null) + } + } + dynamic "auto_healing_policies" { - for_each = var.auto_healing_policies == null ? [] : [var.auto_healing_policies] + for_each = var.auto_healing_policies == null ? [] : [""] iterator = config content { - health_check = config.value.health_check - initial_delay_sec = config.value.initial_delay_sec - } - } - dynamic "stateful_disk" { - for_each = try(var.stateful_config.mig_config.stateful_disks, {}) - iterator = config - content { - device_name = config.key - delete_rule = config.value.delete_rule - } - } - dynamic "update_policy" { - for_each = var.update_policy == null ? [] : [var.update_policy] - iterator = config - content { - type = config.value.type - minimal_action = config.value.minimal_action - min_ready_sec = config.value.min_ready_sec - max_surge_fixed = ( - config.value.max_surge_type == "fixed" ? config.value.max_surge : null - ) - max_surge_percent = ( - config.value.max_surge_type == "percent" ? config.value.max_surge : null - ) - max_unavailable_fixed = ( - config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null - ) - max_unavailable_percent = ( - config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null - ) + health_check = local.health_check + initial_delay_sec = var.auto_healing_policies.initial_delay_sec } } + dynamic "named_port" { for_each = var.named_ports == null ? {} : var.named_ports iterator = config @@ -121,167 +66,88 @@ resource "google_compute_instance_group_manager" "default" { port = config.value } } - version { - instance_template = var.default_version.instance_template - name = var.default_version.name + + dynamic "stateful_disk" { + for_each = var.stateful_disks + content { + device_name = stateful_disk.key + delete_rule = stateful_disk.value + } } + + dynamic "update_policy" { + for_each = var.update_policy == null ? [] : [var.update_policy] + iterator = p + content { + minimal_action = p.value.minimal_action + type = p.value.type + max_surge_fixed = try(p.value.max_surge.fixed, null) + max_surge_percent = try(p.value.max_surge.percent, null) + max_unavailable_fixed = try(p.value.max_unavailable.fixed, null) + max_unavailable_percent = try(p.value.max_unavailable.percent, null) + min_ready_sec = p.value.min_ready_sec + most_disruptive_allowed_action = p.value.most_disruptive_action + replacement_method = p.value.replacement_method + } + } + + version { + instance_template = var.instance_template + name = var.default_version_name + } + dynamic "version" { - for_each = var.versions == null ? {} : var.versions - iterator = version + for_each = var.versions content { name = version.key instance_template = version.value.instance_template - target_size { - fixed = ( - version.value.target_type == "fixed" ? version.value.target_size : null - ) - percent = ( - version.value.target_type == "percent" ? version.value.target_size : null - ) + dynamic "target_size" { + for_each = version.value.target_size == null ? [] : [""] + content { + fixed = version.value.target_size.fixed + percent = version.value.target_size.percent + } } } } } -locals { - instance_group_manager = ( - var.regional ? - google_compute_region_instance_group_manager.default : - google_compute_instance_group_manager.default - ) -} - -resource "google_compute_per_instance_config" "default" { - for_each = try(var.stateful_config.per_instance_config, {}) - #for_each = var.stateful_config && var.stateful_config.per_instance_config == null ? {} : length(var.stateful_config.per_instance_config) - zone = var.location - # terraform error, solved with locals - #instance_group_manager = var.regional ? google_compute_region_instance_group_manager.default : google_compute_instance_group_manager.default - instance_group_manager = local.instance_group_manager[0].id - name = each.key - project = var.project_id - minimal_action = try(each.value.update_config.minimal_action, null) - most_disruptive_allowed_action = try(each.value.update_config.most_disruptive_allowed_action, null) - remove_instance_state_on_destroy = try(each.value.update_config.remove_instance_state_on_destroy, null) - preserved_state { - - metadata = each.value.metadata - - dynamic "disk" { - for_each = try(each.value.stateful_disks, {}) - #for_each = var.stateful_config.mig_config.stateful_disks == null ? {} : var.stateful_config.mig_config.stateful_disks - iterator = config - content { - device_name = config.key - source = config.value.source - mode = config.value.mode - delete_rule = config.value.delete_rule - } - } - } -} - -resource "google_compute_region_autoscaler" "default" { - provider = google-beta - count = var.regional && var.autoscaler_config != null ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - region = var.location - target = google_compute_region_instance_group_manager.default.0.id - - autoscaling_policy { - max_replicas = var.autoscaler_config.max_replicas - min_replicas = var.autoscaler_config.min_replicas - cooldown_period = var.autoscaler_config.cooldown_period - - dynamic "cpu_utilization" { - for_each = ( - var.autoscaler_config.cpu_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.cpu_utilization_target - } - } - - dynamic "load_balancing_utilization" { - for_each = ( - var.autoscaler_config.load_balancing_utilization_target == null ? [] : [""] - ) - content { - target = var.autoscaler_config.load_balancing_utilization_target - } - } - - dynamic "metric" { - for_each = ( - var.autoscaler_config.metric == null - ? [] - : [var.autoscaler_config.metric] - ) - iterator = config - content { - name = config.value.name - single_instance_assignment = config.value.single_instance_assignment - target = config.value.target - type = config.value.type - filter = config.value.filter - } - } - } -} - - resource "google_compute_region_instance_group_manager" "default" { provider = google-beta - count = var.regional ? 1 : 0 + count = local.is_regional ? 1 : 0 project = var.project_id region = var.location name = var.name base_instance_name = var.name - description = "Terraform-managed." - target_size = var.target_size - target_pools = var.target_pools - wait_for_instances = var.wait_for_instances - dynamic "auto_healing_policies" { - for_each = var.auto_healing_policies == null ? [] : [var.auto_healing_policies] - iterator = config + description = var.description + distribution_policy_target_shape = try( + var.distribution_policy.target_shape, null + ) + distribution_policy_zones = try( + var.distribution_policy.zones, null + ) + target_size = var.target_size + target_pools = var.target_pools + wait_for_instances = try(var.wait_for_instances.enabled, null) + wait_for_instances_status = try(var.wait_for_instances.status, null) + + dynamic "all_instances_config" { + for_each = var.all_instances_config == null ? [] : [""] content { - health_check = config.value.health_check - initial_delay_sec = config.value.initial_delay_sec - } - } - dynamic "stateful_disk" { - for_each = try(var.stateful_config.mig_config.stateful_disks, {}) - iterator = config - content { - device_name = config.key - delete_rule = config.value.delete_rule + labels = try(var.all_instances_config.labels, null) + metadata = try(var.all_instances_config.metadata, null) } } - dynamic "update_policy" { - for_each = var.update_policy == null ? [] : [var.update_policy] + dynamic "auto_healing_policies" { + for_each = var.auto_healing_policies == null ? [] : [""] iterator = config content { - instance_redistribution_type = config.value.instance_redistribution_type - type = config.value.type - minimal_action = config.value.minimal_action - min_ready_sec = config.value.min_ready_sec - max_surge_fixed = ( - config.value.max_surge_type == "fixed" ? config.value.max_surge : null - ) - max_surge_percent = ( - config.value.max_surge_type == "percent" ? config.value.max_surge : null - ) - max_unavailable_fixed = ( - config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null - ) - max_unavailable_percent = ( - config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null - ) + health_check = local.health_check + initial_delay_sec = var.auto_healing_policies.initial_delay_sec } } + dynamic "named_port" { for_each = var.named_ports == null ? {} : var.named_ports iterator = config @@ -290,172 +156,49 @@ resource "google_compute_region_instance_group_manager" "default" { port = config.value } } - version { - instance_template = var.default_version.instance_template - name = var.default_version.name + + dynamic "stateful_disk" { + for_each = var.stateful_disks + content { + device_name = stateful_disk.key + delete_rule = stateful_disk.value + } } + + dynamic "update_policy" { + for_each = var.update_policy == null ? [] : [var.update_policy] + iterator = p + content { + minimal_action = p.value.minimal_action + type = p.value.type + instance_redistribution_type = p.value.regional_redistribution_type + max_surge_fixed = try(p.value.max_surge.fixed, null) + max_surge_percent = try(p.value.max_surge.percent, null) + max_unavailable_fixed = try(p.value.max_unavailable.fixed, null) + max_unavailable_percent = try(p.value.max_unavailable.percent, null) + min_ready_sec = p.value.min_ready_sec + most_disruptive_allowed_action = p.value.most_disruptive_action + replacement_method = p.value.replacement_method + } + } + + version { + instance_template = var.instance_template + name = var.default_version_name + } + dynamic "version" { - for_each = var.versions == null ? {} : var.versions - iterator = version + for_each = var.versions content { name = version.key instance_template = version.value.instance_template - target_size { - fixed = ( - version.value.target_type == "fixed" ? version.value.target_size : null - ) - percent = ( - version.value.target_type == "percent" ? version.value.target_size : null - ) + dynamic "target_size" { + for_each = version.value.target_size == null ? [] : [""] + content { + fixed = version.value.target_size.fixed + percent = version.value.target_size.percent + } } } } } - -resource "google_compute_health_check" "http" { - provider = google-beta - count = try(var.health_check_config.type, null) == "http" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - http_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "https" { - provider = google-beta - count = try(var.health_check_config.type, null) == "https" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - https_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "tcp" { - provider = google-beta - count = try(var.health_check_config.type, null) == "tcp" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - tcp_health_check { - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request = try(var.health_check_config.check.request, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "ssl" { - provider = google-beta - count = try(var.health_check_config.type, null) == "ssl" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - ssl_health_check { - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request = try(var.health_check_config.check.request, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} - -resource "google_compute_health_check" "http2" { - provider = google-beta - count = try(var.health_check_config.type, null) == "http2" ? 1 : 0 - project = var.project_id - name = var.name - description = "Terraform managed." - - check_interval_sec = try(var.health_check_config.config.check_interval_sec, null) - healthy_threshold = try(var.health_check_config.config.healthy_threshold, null) - timeout_sec = try(var.health_check_config.config.timeout_sec, null) - unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null) - - http2_health_check { - host = try(var.health_check_config.check.host, null) - port = try(var.health_check_config.check.port, null) - port_name = try(var.health_check_config.check.port_name, null) - port_specification = try(var.health_check_config.check.port_specification, null) - proxy_header = try(var.health_check_config.check.proxy_header, null) - request_path = try(var.health_check_config.check.request_path, null) - response = try(var.health_check_config.check.response, null) - } - - dynamic "log_config" { - for_each = try(var.health_check_config.logging, false) ? [""] : [] - content { - enable = true - } - } -} diff --git a/modules/compute-mig/outputs.tf b/modules/compute-mig/outputs.tf index 93de9223..a7be7d2e 100644 --- a/modules/compute-mig/outputs.tf +++ b/modules/compute-mig/outputs.tf @@ -37,13 +37,6 @@ output "health_check" { value = ( var.health_check_config == null ? null - : try( - google_compute_health_check.http.0, - google_compute_health_check.https.0, - google_compute_health_check.tcp.0, - google_compute_health_check.ssl.0, - google_compute_health_check.http2.0, - {} - ) + : google_compute_health_check.autohealing.0 ) } diff --git a/modules/compute-mig/stateful-config.tf b/modules/compute-mig/stateful-config.tf new file mode 100644 index 00000000..1e9e056e --- /dev/null +++ b/modules/compute-mig/stateful-config.tf @@ -0,0 +1,91 @@ +/** + * Copyright 2022 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 Instance-level stateful configuration resources. + +resource "google_compute_per_instance_config" "default" { + for_each = local.is_regional ? {} : var.stateful_config + project = var.project_id + zone = var.location + name = each.key + instance_group_manager = try( + google_compute_instance_group_manager.default.0.id, null + ) + minimal_action = each.value.minimal_action + most_disruptive_allowed_action = each.value.most_disruptive_action + remove_instance_state_on_destroy = each.value.remove_state_on_destroy + + dynamic "preserved_state" { + for_each = each.value.preserved_state == null ? [] : [""] + content { + metadata = each.value.preserved_state.metadata + dynamic "disk" { + for_each = ( + each.value.preserved_state.disks == null + ? {} + : each.value.preserved_state.disks + ) + content { + device_name = disk.key + source = disk.value.source + delete_rule = ( + disk.value.delete_on_instance_deletion == true + ? "ON_PERMANENT_INSTANCE_DELETION" + : "NEVER" + ) + mode = disk.value.read_only == true ? "READ_ONLY" : "READ_WRITE" + } + } + } + } +} + +resource "google_compute_region_per_instance_config" "default" { + for_each = local.is_regional ? var.stateful_config : {} + project = var.project_id + region = var.location + name = each.key + region_instance_group_manager = try( + google_compute_region_instance_group_manager.default.0.id, null + ) + minimal_action = each.value.minimal_action + most_disruptive_allowed_action = each.value.most_disruptive_action + remove_instance_state_on_destroy = each.value.remove_state_on_destroy + + dynamic "preserved_state" { + for_each = each.value.preserved_state == null ? [] : [""] + content { + metadata = each.value.preserved_state.metadata + dynamic "disk" { + for_each = ( + each.value.preserved_state.disks == null + ? {} + : each.value.preserved_state.disks + ) + content { + device_name = disk.key + source = disk.value.source + delete_rule = ( + disk.value.delete_on_instance_deletion == true + ? "ON_PERMANENT_INSTANCE_DELETION" + : "NEVER" + ) + mode = disk.value.read_only == true ? "READ_ONLY" : "READ_WRITE" + } + } + } + } +} diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf index 76f4fb21..299dacc8 100644 --- a/modules/compute-mig/variables.tf +++ b/modules/compute-mig/variables.tf @@ -14,57 +14,149 @@ * limitations under the License. */ +variable "all_instances_config" { + description = "Metadata and labels set to all instances in the group." + type = object({ + labels = optional(map(string)) + metadata = optional(map(string)) + }) + default = null +} + variable "auto_healing_policies" { description = "Auto-healing policies for this group." type = object({ - health_check = string + health_check = optional(string) initial_delay_sec = number }) default = null } variable "autoscaler_config" { - description = "Optional autoscaler configuration. Only one of 'cpu_utilization_target' 'load_balancing_utilization_target' or 'metric' can be not null." + description = "Optional autoscaler configuration." type = object({ - max_replicas = number - min_replicas = number - cooldown_period = number - cpu_utilization_target = number - load_balancing_utilization_target = number - metric = object({ - name = string - single_instance_assignment = number - target = number - type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE - filter = string - }) + max_replicas = number + min_replicas = number + cooldown_period = optional(number) + mode = optional(string) # OFF, ONLY_UP, ON + scaling_control = optional(object({ + down = optional(object({ + max_replicas_fixed = optional(number) + max_replicas_percent = optional(number) + time_window_sec = optional(number) + })) + in = optional(object({ + max_replicas_fixed = optional(number) + max_replicas_percent = optional(number) + time_window_sec = optional(number) + })) + }), {}) + scaling_signals = optional(object({ + cpu_utilization = optional(object({ + target = number + optimize_availability = optional(bool) + })) + load_balancing_utilization = optional(object({ + target = number + })) + metrics = optional(list(object({ + name = string + type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE + target_value = number + single_instance_assignment = optional(number) + time_series_filter = optional(string) + }))) + schedules = optional(list(object({ + duration_sec = number + name = string + min_required_replicas = number + cron_schedule = string + description = optional(bool) + timezone = optional(string) + disabled = optional(bool) + }))) + }), {}) }) default = null } -variable "default_version" { - description = "Default application version template. Additional versions can be specified via the `versions` variable." +variable "default_version_name" { + description = "Name used for the default version." + type = string + default = "default" +} + +variable "description" { + description = "Optional description used for all resources managed by this module." + type = string + default = "Terraform managed." +} + +variable "distribution_policy" { + description = "DIstribution policy for regional MIG." type = object({ - instance_template = string - name = string + target_shape = optional(string) + zones = optional(list(string)) }) + default = null } variable "health_check_config" { description = "Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage." type = object({ - type = string # http https tcp ssl http2 - check = map(any) # actual health check block attributes - config = map(number) # interval, thresholds, timeout - logging = bool + check_interval_sec = optional(number) + description = optional(string, "Terraform managed.") + enable_logging = optional(bool, false) + healthy_threshold = optional(number) + timeout_sec = optional(number) + unhealthy_threshold = optional(number) + grpc = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + service_name = optional(string) + })) + http = optional(object({ + host = optional(string) + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request_path = optional(string) + response = optional(string) + use_protocol = optional(string, "http") # http http2 https + })) + tcp = optional(object({ + port = optional(number) + port_name = optional(string) + port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT + proxy_header = optional(string) + request = optional(string) + response = optional(string) + use_ssl = optional(bool, false) + })) }) default = null + validation { + condition = ( + (try(var.health_check_config.grpc, null) == null ? 0 : 1) + + (try(var.health_check_config.http, null) == null ? 0 : 1) + + (try(var.health_check_config.tcp, null) == null ? 0 : 1) <= 1 + ) + error_message = "Only one health check type can be configured at a time." + } +} + +variable "instance_template" { + description = "Instance template for the default version." + type = string } variable "location" { - description = "Compute zone, or region if `regional` is set to true." + description = "Compute zone or region." type = string } + variable "name" { description = "Managed group name." type = string @@ -81,41 +173,30 @@ variable "project_id" { type = string } -variable "regional" { - description = "Use regional instance group. When set, `location` should be set to the region." - type = bool - default = false +variable "stateful_disks" { + description = "Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean." + type = map(bool) + default = {} + nullable = false } variable "stateful_config" { - description = "Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name." - type = object({ - per_instance_config = map(object({ - #name is the key - #name = string - stateful_disks = map(object({ - #device_name is the key - source = string - mode = string # READ_WRITE | READ_ONLY - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - metadata = map(string) - update_config = object({ - minimal_action = string # NONE | REPLACE | RESTART | REFRESH - most_disruptive_allowed_action = string # REPLACE | RESTART | REFRESH | NONE - remove_instance_state_on_destroy = bool - }) + description = "Stateful configuration for individual instances." + type = map(object({ + minimal_action = optional(string) + most_disruptive_action = optional(string) + remove_state_on_destroy = optional(bool) + preserved_state = optional(object({ + disks = optional(map(object({ + source = string + delete_on_instance_deletion = optional(bool) + read_only = optional(bool) + }))) + metadata = optional(map(string)) })) - - mig_config = object({ - stateful_disks = map(object({ - #device_name is the key - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - }) - - }) - default = null + })) + default = {} + nullable = false } variable "target_pools" { @@ -131,32 +212,44 @@ variable "target_size" { } variable "update_policy" { - description = "Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'." + description = "Update policy. Minimal action and type are required." type = object({ - instance_redistribution_type = optional(string, "PROACTIVE") # NONE | PROACTIVE. The attribute is ignored if regional is set to false. - max_surge_type = string # fixed | percent - max_surge = number - max_unavailable_type = string - max_unavailable = number - minimal_action = string # REPLACE | RESTART - min_ready_sec = number - type = string # OPPORTUNISTIC | PROACTIVE + minimal_action = string + type = string + max_surge = optional(object({ + fixed = optional(number) + percent = optional(number) + })) + max_unavailable = optional(object({ + fixed = optional(number) + percent = optional(number) + })) + min_ready_sec = optional(number) + most_disruptive_action = optional(string) + regional_redistribution_type = optional(string) + replacement_method = optional(string) }) default = null } variable "versions" { - description = "Additional application versions, target_type is either 'fixed' or 'percent'." + description = "Additional application versions, target_size is optional." type = map(object({ instance_template = string - target_type = string # fixed | percent - target_size = number + target_size = optional(object({ + fixed = optional(number) + percent = optional(number) + })) })) - default = null + default = {} + nullable = false } variable "wait_for_instances" { description = "Wait for all instances to be created/updated before returning." - type = bool - default = null + type = object({ + enabled = bool + status = optional(string) + }) + default = null } diff --git a/modules/folder/README.md b/modules/folder/README.md index 8f4c1bc3..d543004a 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -26,25 +26,53 @@ module "folder" { ### Organization policies +To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. + ```hcl module "folder" { source = "./fabric/modules/folder" parent = "organizations/1234567890" name = "Folder name" - policy_boolean = { - "constraints/compute.disableGuestAttributesAccess" = true - "constraints/compute.skipDefaultNetworkCreation" = true - } - policy_list = { + org_policies = { + "compute.disableGuestAttributesAccess" = { + enforce = true + } + "constraints/compute.skipDefaultNetworkCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + title = "condition" + description = "test condition" + location = "somewhere" + } + enforce = true + } + ] + } + "constraints/iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } "constraints/compute.trustedImageProjects" = { - inherit_from_parent = null - suggested_value = null - status = true - values = ["projects/my-project"] + allow = { + values = ["projects/my-project"] + } + } + "constraints/compute.vmExternalIpAccess" = { + deny = { all = true } } } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=8 ``` ### Firewall policy factory @@ -259,7 +287,7 @@ module "folder" { | [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding · google_folder_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_folder_exclusion · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact · google_folder | -| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy | +| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [tags.tf](./tags.tf) | None | google_tags_tag_binding | | [variables.tf](./variables.tf) | Module variables. | | @@ -282,10 +310,9 @@ module "folder" { | [logging_exclusions](variables.tf#L98) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L105) | Logging sinks to create for this folder. | map(object({…})) | | {} | | [name](variables.tf#L126) | Folder name. | string | | null | -| [parent](variables.tf#L132) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [policy_boolean](variables.tf#L142) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L149) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L161) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [org_policies](variables.tf#L132) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L172) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L182) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs @@ -295,7 +322,7 @@ module "folder" { | [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | | | [folder](outputs.tf#L26) | Folder resource. | | | [id](outputs.tf#L31) | Folder id. | | -| [name](outputs.tf#L41) | Folder name. | | -| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | | +| [name](outputs.tf#L40) | Folder name. | | +| [sink_writer_identities](outputs.tf#L45) | Writer identities created for each sink. | | diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 177a3d80..da478063 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -16,75 +16,79 @@ # tfdoc:file:description Folder-level organization policies. -resource "google_folder_organization_policy" "boolean" { - for_each = var.policy_boolean - folder = local.folder.name - constraint = each.key - - dynamic "boolean_policy" { - for_each = each.value == null ? [] : [each.value] - iterator = policy - content { - enforced = policy.value - } - } - - dynamic "restore_policy" { - for_each = each.value == null ? [""] : [] - content { - default = true - } +locals { + org_policies = { + for k, v in var.org_policies : + k => merge(v, { + is_boolean_policy = v.allow == null && v.deny == null + has_values = ( + length(coalesce(try(v.allow.values, []), [])) > 0 || + length(coalesce(try(v.deny.values, []), [])) > 0 + ) + rules = [ + for r in v.rules : + merge(r, { + has_values = ( + length(coalesce(try(r.allow.values, []), [])) > 0 || + length(coalesce(try(r.deny.values, []), [])) > 0 + ) + }) + ] + }) } } -resource "google_folder_organization_policy" "list" { - for_each = var.policy_list - folder = local.folder.name - constraint = each.key +resource "google_org_policy_policy" "default" { + for_each = local.org_policies + name = "${local.folder.name}/policies/${each.key}" + parent = local.folder.name - dynamic "list_policy" { - for_each = each.value.status == null ? [] : [each.value] - iterator = policy - content { - inherit_from_parent = policy.value.inherit_from_parent - suggested_value = policy.value.suggested_value - dynamic "allow" { - for_each = policy.value.status ? [""] : [] + spec { + inherit_from_parent = each.value.inherit_from_parent + reset = each.value.reset + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) } } - dynamic "deny" { - for_each = policy.value.status ? [] : [""] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) + } + + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null + deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + condition { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } + dynamic "values" { + for_each = rule.value.has_values ? [1] : [] + content { + allowed_values = try(rule.value.allow.values, null) + denied_values = try(rule.value.deny.values, null) + } } } } } - - dynamic "restore_policy" { - for_each = each.value.status == null ? [true] : [] - content { - default = true - } - } } diff --git a/modules/folder/outputs.tf b/modules/folder/outputs.tf index 37babc6f..8073951b 100644 --- a/modules/folder/outputs.tf +++ b/modules/folder/outputs.tf @@ -33,8 +33,7 @@ output "id" { value = local.folder.name depends_on = [ google_folder_iam_binding.authoritative, - google_folder_organization_policy.boolean, - google_folder_organization_policy.list + google_org_policy_policy.default, ] } diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 19ed18f3..a00e147f 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -129,6 +129,46 @@ variable "name" { default = null } +variable "org_policies" { + description = "Organization policies applied to this folder keyed by policy name." + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + 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, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false +} + variable "parent" { description = "Parent in folders/folder_id or organizations/org_id format." type = string @@ -139,25 +179,6 @@ variable "parent" { } } -variable "policy_boolean" { - description = "Map of boolean org policies and enforcement value, set value to null for policy restore." - type = map(bool) - default = {} - nullable = false -} - -variable "policy_list" { - description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} - nullable = false -} - variable "tag_bindings" { description = "Tag bindings for this folder, in key => tag value id format." type = map(string) diff --git a/modules/organization/README.md b/modules/organization/README.md index 2377c6cc..3c57b743 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -7,6 +7,8 @@ This module allows managing several organization properties: - audit logging configuration for services - organization policies +To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. + ## Example ```hcl @@ -19,20 +21,47 @@ module "org" { iam = { "roles/resourcemanager.projectCreator" = ["group:cloud-admins@example.org"] } - policy_boolean = { - "constraints/compute.disableGuestAttributesAccess" = true - "constraints/compute.skipDefaultNetworkCreation" = true - } - policy_list = { + + org_policies = { + "compute.disableGuestAttributesAccess" = { + enforce = true + } + "constraints/compute.skipDefaultNetworkCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + title = "condition" + description = "test condition" + location = "somewhere" + } + enforce = true + } + ] + } + "constraints/iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } "constraints/compute.trustedImageProjects" = { - inherit_from_parent = null - suggested_value = null - status = true - values = ["projects/my-project"] + allow = { + values = ["projects/my-project"] + } + } + "constraints/compute.vmExternalIpAccess" = { + deny = { all = true } } } } -# tftest modules=1 resources=6 +# tftest modules=1 resources=10 ``` ## IAM @@ -281,7 +310,7 @@ module "org" { | [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_organization_iam_audit_config · google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member · google_organization_iam_policy | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_organization_exclusion · google_logging_organization_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact | -| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_organization_policy | +| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [tags.tf](./tags.tf) | None | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_value · google_tags_tag_value_iam_binding | | [variables.tf](./variables.tf) | Module variables. | | @@ -291,7 +320,7 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L151) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L191) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | | [firewall_policies](variables.tf#L31) | Hierarchical firewall policy rules created in the organization. | map(map(object({…}))) | | {} | @@ -306,10 +335,9 @@ module "org" { | [iam_bindings_authoritative](variables.tf#L116) | IAM authoritative bindings, in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared. Bindings should also be authoritative when using authoritative audit config. Use with caution. | map(list(string)) | | null | | [logging_exclusions](variables.tf#L122) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L129) | Logging sinks to create for this organization. | map(object({…})) | | {} | -| [policy_boolean](variables.tf#L160) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L167) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L179) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L185) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | +| [org_policies](variables.tf#L151) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L200) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L206) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | ## Outputs @@ -320,8 +348,8 @@ module "org" { | [firewall_policies](outputs.tf#L36) | Map of firewall policy resources created in the organization. | | | [firewall_policy_id](outputs.tf#L41) | Map of firewall policy ids created in the organization. | | | [organization_id](outputs.tf#L46) | Organization id dependent on module resources. | | -| [sink_writer_identities](outputs.tf#L64) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L72) | Tag key resources. | | -| [tag_values](outputs.tf#L79) | Tag value resources. | | +| [sink_writer_identities](outputs.tf#L63) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L71) | Tag key resources. | | +| [tag_values](outputs.tf#L78) | Tag value resources. | | diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index f23a98b4..defa11b0 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -16,83 +16,79 @@ # tfdoc:file:description Organization-level organization policies. -resource "google_organization_policy" "boolean" { - for_each = var.policy_boolean - org_id = local.organization_id_numeric - constraint = each.key - - dynamic "boolean_policy" { - for_each = each.value == null ? [] : [each.value] - iterator = policy - content { - enforced = policy.value - } +locals { + org_policies = { + for k, v in var.org_policies : + k => merge(v, { + is_boolean_policy = v.allow == null && v.deny == null + has_values = ( + length(coalesce(try(v.allow.values, []), [])) > 0 || + length(coalesce(try(v.deny.values, []), [])) > 0 + ) + rules = [ + for r in v.rules : + merge(r, { + has_values = ( + length(coalesce(try(r.allow.values, []), [])) > 0 || + length(coalesce(try(r.deny.values, []), [])) > 0 + ) + }) + ] + }) } +} - dynamic "restore_policy" { - for_each = each.value == null ? [""] : [] - content { - default = true - } - } - - depends_on = [ - google_organization_iam_audit_config.config, - google_organization_iam_binding.authoritative, - google_organization_iam_custom_role.roles, - google_organization_iam_member.additive, - google_organization_iam_policy.authoritative, - ] -} - -resource "google_organization_policy" "list" { - for_each = var.policy_list - org_id = local.organization_id_numeric - constraint = each.key - - dynamic "list_policy" { - for_each = each.value.status == null ? [] : [each.value] - iterator = policy - content { - inherit_from_parent = policy.value.inherit_from_parent - suggested_value = policy.value.suggested_value - dynamic "allow" { - for_each = policy.value.status ? [""] : [] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) - } - } - dynamic "deny" { - for_each = policy.value.status ? [] : [""] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) - } - } - } - } - - dynamic "restore_policy" { - for_each = each.value.status == null ? [true] : [] - content { - default = true +resource "google_org_policy_policy" "default" { + for_each = local.org_policies + name = "${var.organization_id}/policies/${each.key}" + parent = var.organization_id + + spec { + inherit_from_parent = each.value.inherit_from_parent + reset = each.value.reset + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] + content { + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) + } + } + } + + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null + deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + condition { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } + dynamic "values" { + for_each = rule.value.has_values ? [1] : [] + content { + allowed_values = try(rule.value.allow.values, null) + denied_values = try(rule.value.deny.values, null) + } + } + } } } @@ -103,4 +99,5 @@ resource "google_organization_policy" "list" { google_organization_iam_member.additive, google_organization_iam_policy.authoritative, ] + } diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index 1679a1d7..198b3c8d 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -52,8 +52,7 @@ output "organization_id" { google_organization_iam_custom_role.roles, google_organization_iam_member.additive, google_organization_iam_policy.authoritative, - google_organization_policy.boolean, - google_organization_policy.list, + google_org_policy_policy.default, google_tags_tag_key.default, google_tags_tag_key_iam_binding.default, google_tags_tag_value.default, diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 9ffce95c..7499d6d6 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -148,6 +148,46 @@ variable "logging_sinks" { nullable = false } +variable "org_policies" { + description = "Organization policies applied to this organization keyed by policy name." + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + 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, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false +} + variable "organization_id" { description = "Organization id in organizations/nnnnnn format." type = string @@ -157,25 +197,6 @@ variable "organization_id" { } } -variable "policy_boolean" { - description = "Map of boolean org policies and enforcement value, set value to null for policy restore." - type = map(bool) - default = {} - nullable = false -} - -variable "policy_list" { - description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} - nullable = false -} - variable "tag_bindings" { description = "Tag bindings for this organization, in key => tag value id format." type = map(string) diff --git a/modules/project/README.md b/modules/project/README.md index 9df30d18..dbb66fcd 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -156,6 +156,8 @@ module "project" { ## Organization policies +To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. + ```hcl module "project" { source = "./fabric/modules/project" @@ -167,20 +169,46 @@ module "project" { "container.googleapis.com", "stackdriver.googleapis.com" ] - policy_boolean = { - "constraints/compute.disableGuestAttributesAccess" = true - "constraints/compute.skipDefaultNetworkCreation" = true - } - policy_list = { + org_policies = { + "compute.disableGuestAttributesAccess" = { + enforce = true + } + "constraints/compute.skipDefaultNetworkCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\"tagKeys/1234\", \"tagValues/1234\")" + title = "condition" + description = "test condition" + location = "somewhere" + } + enforce = true + } + ] + } + "constraints/iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } "constraints/compute.trustedImageProjects" = { - inherit_from_parent = null - suggested_value = null - status = true - values = ["projects/my-project"] + allow = { + values = ["projects/my-project"] + } + } + "constraints/compute.vmExternalIpAccess" = { + deny = { all = true } } } } -# tftest modules=1 resources=6 +# tftest modules=1 resources=10 ``` ## Logging Sinks @@ -349,7 +377,7 @@ output "compute_robot" { | [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_logging_project_exclusion · google_logging_project_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item · google_essential_contacts_contact · google_monitoring_monitored_project · google_project · google_project_service · google_resource_manager_lien | -| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy | +| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member · google_project_default_service_accounts · google_project_iam_member · google_project_service_identity | | [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_project_iam_member | @@ -367,8 +395,8 @@ output "compute_robot" { | [billing_account](variables.tf#L23) | Billing account id. | string | | null | | [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [default_service_account](variables.tf#L49) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | -| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [default_service_account](variables.tf#L43) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | +| [descriptive_name](variables.tf#L49) | Name of the project name. Used for project name instead of `name` variable. | string | | null | | [group_iam](variables.tf#L55) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L62) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_additive](variables.tf#L69) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | @@ -378,23 +406,22 @@ output "compute_robot" { | [logging_exclusions](variables.tf#L95) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L102) | Logging sinks to create for this project. | map(object({…})) | | {} | | [metric_scopes](variables.tf#L124) | List of projects that will act as metric scopes for this project. | list(string) | | [] | -| [oslogin](variables.tf#L136) | Enable OS Login. | bool | | false | -| [oslogin_admins](variables.tf#L142) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | -| [oslogin_users](variables.tf#L150) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | -| [parent](variables.tf#L157) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [policy_boolean](variables.tf#L167) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool) | | {} | -| [policy_list](variables.tf#L174) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…})) | | {} | -| [prefix](variables.tf#L186) | Prefix used to generate project id and name. | string | | null | -| [project_create](variables.tf#L192) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | -| [service_config](variables.tf#L198) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L210) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L217) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | -| [service_perimeter_standard](variables.tf#L224) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L230) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L236) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L245) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | -| [skip_delete](variables.tf#L255) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L261) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [org_policies](variables.tf#L136) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [oslogin](variables.tf#L176) | Enable OS Login. | bool | | false | +| [oslogin_admins](variables.tf#L182) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string) | | [] | +| [oslogin_users](variables.tf#L190) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string) | | [] | +| [parent](variables.tf#L197) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L207) | Prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L213) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L219) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L231) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L238) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L245) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L251) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L257) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L266) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | null | +| [skip_delete](variables.tf#L276) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L282) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs @@ -402,9 +429,9 @@ output "compute_robot" { |---|---|:---:| | [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | | | [name](outputs.tf#L25) | Project name. | | -| [number](outputs.tf#L38) | Project number. | | -| [project_id](outputs.tf#L56) | Project id. | | -| [service_accounts](outputs.tf#L76) | Product robot service accounts in project. | | -| [sink_writer_identities](outputs.tf#L92) | Writer identities created for each sink. | | +| [number](outputs.tf#L37) | Project number. | | +| [project_id](outputs.tf#L54) | Project id. | | +| [service_accounts](outputs.tf#L73) | Product robot service accounts in project. | | +| [sink_writer_identities](outputs.tf#L89) | Writer identities created for each sink. | | diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index 68707548..ae4a8501 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -16,75 +16,79 @@ # tfdoc:file:description Project-level organization policies. -resource "google_project_organization_policy" "boolean" { - for_each = var.policy_boolean - project = local.project.project_id - constraint = each.key - - dynamic "boolean_policy" { - for_each = each.value == null ? [] : [each.value] - iterator = policy - content { - enforced = policy.value - } - } - - dynamic "restore_policy" { - for_each = each.value == null ? [""] : [] - content { - default = true - } +locals { + org_policies = { + for k, v in var.org_policies : + k => merge(v, { + is_boolean_policy = v.allow == null && v.deny == null + has_values = ( + length(coalesce(try(v.allow.values, []), [])) > 0 || + length(coalesce(try(v.deny.values, []), [])) > 0 + ) + rules = [ + for r in v.rules : + merge(r, { + has_values = ( + length(coalesce(try(r.allow.values, []), [])) > 0 || + length(coalesce(try(r.deny.values, []), [])) > 0 + ) + }) + ] + }) } } -resource "google_project_organization_policy" "list" { - for_each = var.policy_list - project = local.project.project_id - constraint = each.key +resource "google_org_policy_policy" "default" { + for_each = local.org_policies + name = "projects/${local.project.project_id}/policies/${each.key}" + parent = "projects/${local.project.project_id}" - dynamic "list_policy" { - for_each = each.value.status == null ? [] : [each.value] - iterator = policy - content { - inherit_from_parent = policy.value.inherit_from_parent - suggested_value = policy.value.suggested_value - dynamic "allow" { - for_each = policy.value.status ? [""] : [] + spec { + inherit_from_parent = each.value.inherit_from_parent + reset = each.value.reset + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) } } - dynamic "deny" { - for_each = policy.value.status ? [] : [""] - content { - values = ( - try(length(policy.value.values) > 0, false) - ? policy.value.values - : null - ) - all = ( - try(length(policy.value.values) > 0, false) - ? null - : true - ) + } + + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null + deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + condition { + description = rule.value.condition.description + expression = rule.value.condition.expression + location = rule.value.condition.location + title = rule.value.condition.title + } + dynamic "values" { + for_each = rule.value.has_values ? [1] : [] + content { + allowed_values = try(rule.value.allow.values, null) + denied_values = try(rule.value.deny.values, null) + } } } } } - - dynamic "restore_policy" { - for_each = each.value.status == null ? [true] : [] - content { - default = true - } - } } diff --git a/modules/project/outputs.tf b/modules/project/outputs.tf index 3b7efc90..cb940d01 100644 --- a/modules/project/outputs.tf +++ b/modules/project/outputs.tf @@ -26,8 +26,7 @@ output "name" { description = "Project name." value = local.project.name depends_on = [ - google_project_organization_policy.boolean, - google_project_organization_policy.list, + google_org_policy_policy.default, google_project_service.project_services, google_compute_shared_vpc_service_project.service_projects, google_project_iam_member.shared_vpc_host_robots, @@ -39,8 +38,7 @@ output "number" { description = "Project number." value = local.project.number depends_on = [ - google_project_organization_policy.boolean, - google_project_organization_policy.list, + google_org_policy_policy.default, google_project_service.project_services, google_compute_shared_vpc_host_project.shared_vpc_host, google_compute_shared_vpc_service_project.shared_vpc_service, @@ -59,8 +57,7 @@ output "project_id" { depends_on = [ google_project.project, data.google_project.project, - google_project_organization_policy.boolean, - google_project_organization_policy.list, + google_org_policy_policy.default, google_project_service.project_services, google_compute_shared_vpc_host_project.shared_vpc_host, google_compute_shared_vpc_service_project.shared_vpc_service, diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 41d3163f..a58affc9 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -40,18 +40,18 @@ variable "custom_roles" { nullable = false } -variable "descriptive_name" { - description = "Name of the project name. Used for project name instead of `name` variable." - type = string - default = null -} - variable "default_service_account" { description = "Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`." default = "keep" type = string } +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + variable "group_iam" { description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable." type = map(list(string)) @@ -133,6 +133,46 @@ variable "name" { type = string } +variable "org_policies" { + description = "Organization policies applied to this project keyed by policy name." + type = map(object({ + inherit_from_parent = optional(bool) # for list policies only. + reset = optional(bool) + + # default (unconditional) values + allow = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + deny = optional(object({ + all = optional(bool) + values = optional(list(string)) + })) + enforce = optional(bool, true) # for boolean policies only. + + # conditional values + 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, true) # for boolean policies only. + condition = object({ + description = optional(string) + expression = optional(string) + location = optional(string) + title = optional(string) + }) + })), []) + })) + default = {} + nullable = false +} + variable "oslogin" { description = "Enable OS Login." type = bool @@ -164,25 +204,6 @@ variable "parent" { } } -variable "policy_boolean" { - description = "Map of boolean org policies and enforcement value, set value to null for policy restore." - type = map(bool) - default = {} - nullable = false -} - -variable "policy_list" { - description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny." - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} - nullable = false -} - variable "prefix" { description = "Prefix used to generate project id and name." type = string diff --git a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py index 0e4b77f5..1b51472c 100644 --- a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py +++ b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py @@ -12,11 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. - import os import pytest - FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') @@ -24,4 +22,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner(FIXTURES_DIR) assert len(modules) == 41 - assert len(resources) == 314 + assert len(resources) == 315 diff --git a/tests/blueprints/data_solutions/data_playground/test_plan.py b/tests/blueprints/data_solutions/data_playground/test_plan.py index 05bda08c..2653c7ea 100644 --- a/tests/blueprints/data_solutions/data_playground/test_plan.py +++ b/tests/blueprints/data_solutions/data_playground/test_plan.py @@ -12,15 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. - import os import pytest - FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner(FIXTURES_DIR) assert len(modules) == 7 - assert len(resources) == 34 + assert len(resources) == 35 diff --git a/tests/modules/compute_mig/fixture/main.tf b/tests/modules/compute_mig/fixture/main.tf index 5d87f40f..b91c0140 100644 --- a/tests/modules/compute_mig/fixture/main.tf +++ b/tests/modules/compute_mig/fixture/main.tf @@ -24,21 +24,18 @@ resource "google_compute_disk" "default" { } module "test" { - source = "../../../../modules/compute-mig" - project_id = "my-project" - location = "europe-west1" - name = "test-mig" - target_size = 2 - default_version = { - instance_template = "foo-template" - name = "foo" - } - autoscaler_config = var.autoscaler_config - health_check_config = var.health_check_config - named_ports = var.named_ports - regional = var.regional - stateful_config = var.stateful_config - - update_policy = var.update_policy - versions = var.versions + source = "../../../../modules/compute-mig" + project_id = "my-project" + name = "test-mig" + target_size = 2 + default_version_name = "foo" + instance_template = "foo-template" + location = var.location + autoscaler_config = var.autoscaler_config + health_check_config = var.health_check_config + named_ports = var.named_ports + stateful_config = var.stateful_config + stateful_disks = var.stateful_disks + update_policy = var.update_policy + versions = var.versions } diff --git a/tests/modules/compute_mig/fixture/variables.tf b/tests/modules/compute_mig/fixture/variables.tf index b9fde834..70117838 100644 --- a/tests/modules/compute_mig/fixture/variables.tf +++ b/tests/modules/compute_mig/fixture/variables.tf @@ -14,101 +14,82 @@ * limitations under the License. */ -variable "autoscaler_config" { - type = object({ - max_replicas = number - min_replicas = number - cooldown_period = number - cpu_utilization_target = number - load_balancing_utilization_target = number - metric = object({ - name = string - single_instance_assignment = number - target = number - type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE - filter = string - }) - }) +variable "all_instances_config" { + type = any default = null } variable "auto_healing_policies" { - type = object({ - health_check = string - initial_delay_sec = number - }) + type = any + default = null +} + +variable "autoscaler_config" { + type = any + default = null +} + +variable "default_version_name" { + type = any + default = "default" +} + +variable "description" { + type = any + default = "Terraform managed." +} + +variable "distribution_policy" { + type = any default = null } variable "health_check_config" { - type = object({ - type = string # http https tcp ssl http2 - check = map(any) # actual health check block attributes - config = map(number) # interval, thresholds, timeout - logging = bool - }) + type = any default = null } +variable "location" { + type = any + default = "europe-west1-b" +} + variable "named_ports" { - type = map(number) + type = any default = null } -variable "regional" { - type = bool - default = false +variable "stateful_disks" { + type = any + default = {} } variable "stateful_config" { - description = "Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name." - type = object({ - per_instance_config = map(object({ - #name is the key - #name = string - stateful_disks = map(object({ - #device_name is the key - source = string - mode = string # READ_WRITE | READ_ONLY - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - metadata = map(string) - update_config = object({ - minimal_action = string # NONE | REPLACE | RESTART | REFRESH - most_disruptive_allowed_action = string # REPLACE | RESTART | REFRESH | NONE - remove_instance_state_on_destroy = bool - }) - })) + type = any + default = {} +} - mig_config = object({ - stateful_disks = map(object({ - #device_name is the key - delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION - })) - }) +variable "target_pools" { + type = any + default = [] +} - }) +variable "target_size" { + type = any default = null } variable "update_policy" { - type = object({ - type = string # OPPORTUNISTIC | PROACTIVE - minimal_action = string # REPLACE | RESTART - min_ready_sec = number - max_surge_type = string # fixed | percent - max_surge = number - max_unavailable_type = string - max_unavailable = number - }) + type = any default = null } variable "versions" { - type = map(object({ - instance_template = string - target_type = string # fixed | percent - target_size = number - })) + type = any + default = {} +} + +variable "wait_for_instances" { + type = any default = null } diff --git a/tests/modules/compute_mig/test_plan.py b/tests/modules/compute_mig/test_plan.py index 253e27bc..e24a7ca7 100644 --- a/tests/modules/compute_mig/test_plan.py +++ b/tests/modules/compute_mig/test_plan.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_defaults(plan_runner): "Test variable defaults." _, resources = plan_runner() @@ -21,7 +22,7 @@ def test_defaults(plan_runner): assert mig['type'] == 'google_compute_instance_group_manager' assert mig['values']['target_size'] == 2 assert mig['values']['zone'] - _, resources = plan_runner(regional='true') + _, resources = plan_runner(location='"europe-west1"') assert len(resources) == 1 mig = resources[0] assert mig['type'] == 'google_compute_region_instance_group_manager' @@ -31,7 +32,12 @@ def test_defaults(plan_runner): def test_health_check(plan_runner): "Test health check resource." - health_check_config = '{type="tcp", check={port=80}, config=null, logging=false}' + health_check_config = '''{ + enable_logging = true + tcp = { + port = 80 + } + }''' _, resources = plan_runner(health_check_config=health_check_config) assert len(resources) == 2 assert any(r['type'] == 'google_compute_health_check' for r in resources) @@ -39,20 +45,26 @@ def test_health_check(plan_runner): def test_autoscaler(plan_runner): "Test autoscaler resource." - autoscaler_config = ( - '{' - 'max_replicas=3, min_replicas=1, cooldown_period=60,' - 'cpu_utilization_target=65, load_balancing_utilization_target=null,' - 'metric=null' - '}' - ) + autoscaler_config = '''{ + colldown_period = 60 + max_replicas = 3 + min_replicas = 1 + scaling_signals = { + cpu_utilization = { + target = 65 + } + } + }''' _, resources = plan_runner(autoscaler_config=autoscaler_config) assert len(resources) == 2 autoscaler = resources[0] assert autoscaler['type'] == 'google_compute_autoscaler' assert autoscaler['values']['autoscaling_policy'] == [{ 'cooldown_period': 60, - 'cpu_utilization': [{'predictive_method': 'NONE', 'target': 65}], + 'cpu_utilization': [{ + 'predictive_method': 'NONE', + 'target': 65 + }], 'load_balancing_utilization': [], 'max_replicas': 3, 'metric': [], @@ -62,7 +74,7 @@ def test_autoscaler(plan_runner): 'scaling_schedules': [], }] _, resources = plan_runner(autoscaler_config=autoscaler_config, - regional='true') + location='"europe-west1"') assert len(resources) == 2 autoscaler = resources[0] assert autoscaler['type'] == 'google_compute_region_autoscaler' @@ -71,17 +83,10 @@ def test_autoscaler(plan_runner): def test_stateful_mig(plan_runner): "Test stateful instances - mig." - stateful_config = ( - '{' - 'per_instance_config = {},' - 'mig_config = {' - 'stateful_disks = {' - 'persistent-disk-1 = {delete_rule="NEVER"}' - '}' - '}' - '}' - ) - _, resources = plan_runner(stateful_config=stateful_config) + stateful_disks = '''{ + persistent-disk-1 = null + }''' + _, resources = plan_runner(stateful_disks=stateful_disks) assert len(resources) == 1 statefuldisk = resources[0] assert statefuldisk['type'] == 'google_compute_instance_group_manager' @@ -93,35 +98,19 @@ def test_stateful_mig(plan_runner): def test_stateful_instance(plan_runner): "Test stateful instances - instance." - stateful_config = ( - '{' - 'per_instance_config = {' - 'instance-1 = {' - 'stateful_disks = {' - 'persistent-disk-1 = {' - 'source = "test-disk",' - 'mode = "READ_ONLY",' - 'delete_rule= "NEVER",' - '},' - '},' - 'metadata = {' - 'foo = "bar"' - '},' - 'update_config = {' - 'minimal_action = "NONE",' - 'most_disruptive_allowed_action = "REPLACE",' - 'remove_instance_state_on_destroy = false,' - - '},' - '},' - '},' - 'mig_config = {' - 'stateful_disks = {' - 'persistent-disk-1 = {delete_rule="NEVER"}' - '}' - '}' - '}' - ) + stateful_config = '''{ + instance-1 = { + most_disruptive_action = "REPLACE", + preserved_state = { + disks = { + persistent-disk-1 = { + source = "test-disk" + } + } + metadata = { foo = "bar" } + } + } + }''' _, resources = plan_runner(stateful_config=stateful_config) assert len(resources) == 2 instanceconfig = resources[0] @@ -134,13 +123,12 @@ def test_stateful_instance(plan_runner): 'device_name': 'persistent-disk-1', 'delete_rule': 'NEVER', 'source': 'test-disk', - 'mode': 'READ_ONLY', + 'mode': 'READ_WRITE', }], 'metadata': { 'foo': 'bar' } }] - assert instanceconfig['values']['minimal_action'] == 'NONE' assert instanceconfig['values']['most_disruptive_allowed_action'] == 'REPLACE' assert instanceconfig['values']['remove_instance_state_on_destroy'] == False diff --git a/tests/modules/folder/fixture/main.tf b/tests/modules/folder/fixture/main.tf index 2fa1b4fd..8290f82e 100644 --- a/tests/modules/folder/fixture/main.tf +++ b/tests/modules/folder/fixture/main.tf @@ -22,10 +22,9 @@ module "test" { iam = var.iam iam_additive = var.iam_additive iam_additive_members = var.iam_additive_members - policy_boolean = var.policy_boolean - policy_list = var.policy_list firewall_policies = var.firewall_policies firewall_policy_association = var.firewall_policy_association logging_sinks = var.logging_sinks logging_exclusions = var.logging_exclusions + org_policies = var.org_policies } diff --git a/tests/modules/folder/fixture/variables.tf b/tests/modules/folder/fixture/variables.tf index da676deb..7c03e056 100644 --- a/tests/modules/folder/fixture/variables.tf +++ b/tests/modules/folder/fixture/variables.tf @@ -34,16 +34,6 @@ variable "iam_additive_members" { default = {} } -variable "policy_boolean" { - type = any - default = {} -} - -variable "policy_list" { - type = any - default = {} -} - variable "firewall_policies" { type = any default = {} @@ -63,3 +53,8 @@ variable "logging_exclusions" { type = any default = {} } + +variable "org_policies" { + type = any + default = {} +} diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py index b7ae96c2..f84d50fb 100644 --- a/tests/modules/folder/test_plan_org_policies.py +++ b/tests/modules/folder/test_plan_org_policies.py @@ -12,56 +12,212 @@ # See the License for the specific language governing permissions and # limitations under the License. -def test_sink(plan_runner): - "Test folder-level sink." - policy_boolean = '{policy-a = true, policy-b = false, policy-c = null}' - _, resources = plan_runner(policy_boolean=policy_boolean) +def test_policy_boolean(plan_runner): + "Test boolean org policy." + policies = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 3 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 2 + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyCreation' + ][0] + + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyUpload' + ][0] + + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert len(p2['rules']) == 2 + assert p2['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'FALSE', + 'values': [] + } + assert p2['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + } + + +def test_policy_list(plan_runner): + "Test list org policy." + policies = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) assert len(resources) == 4 - resources = [r for r in resources if r['type'] - == 'google_folder_organization_policy'] - assert sorted([r['index'] for r in resources]) == [ - 'policy-a', - 'policy-b', - 'policy-c', - ] - policy_values = [] - for resource in resources: - for policy in ('boolean_policy', 'restore_policy'): - value = resource['values'][policy] - if value: - policy_values.append((resource['index'], policy,) + value[0].popitem()) - assert sorted(policy_values) == [ - ('policy-a', 'boolean_policy', 'enforced', True), - ('policy-b', 'boolean_policy', 'enforced', False), - ('policy-c', 'restore_policy', 'default', True), - ] + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 3 -def test_exclussions(plan_runner): - "Test folder-level logging exclusions." - policy_list = ( - '{' - 'policy-a = {inherit_from_parent = true, suggested_value = null, status = true, values = []}, ' - 'policy-b = {inherit_from_parent = null, suggested_value = "foo", status = false, values = ["bar"]}, ' - 'policy-c = {inherit_from_parent = null, suggested_value = true, status = null, values = null}' - '}' - ) - _, resources = plan_runner(policy_list=policy_list) - assert len(resources) == 4 - resources = [r for r in resources if r['type'] - == 'google_folder_organization_policy'] - assert sorted([r['index'] for r in resources]) == [ - 'policy-a', - 'policy-b', - 'policy-c', - ] - values = [r['values'] for r in resources] - assert [r['constraint'] for r in values] == [ - 'policy-a', 'policy-b', 'policy-c' - ] - assert values[0]['list_policy'][0]['allow'] == [ - {'all': True, 'values': None}] - assert values[1]['list_policy'][0]['deny'] == [ - {'all': False, 'values': ["bar"]}] - assert values[2]['restore_policy'] == [{'default': True}] + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.vmExternalIpAccess' + ][0] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': 'TRUE', + 'enforce': None, + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.allowedPolicyMemberDomains' + ][0] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert p2['rules'] == [{ + 'allow_all': + None, + 'condition': [], + 'deny_all': + None, + 'enforce': + None, + 'values': [{ + 'allowed_values': [ + 'C0xxxxxxx', + 'C0yyyyyyy', + ], + 'denied_values': None + }] + }] + + p3 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' + ][0] + assert p3['inherit_from_parent'] is None + assert p3['reset'] is None + assert len(p3['rules']) == 3 + assert p3['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': None, + 'denied_values': ['in:EXTERNAL'] + }] + } + + assert p3['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': ['EXTERNAL_1'], + 'denied_values': None + }] + } + + assert p3['rules'][2] == { + 'allow_all': 'TRUE', + 'condition': [{ + 'description': + 'test condition2', + 'expression': + 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', + 'location': + 'xxx', + 'title': + 'condition2' + }], + 'deny_all': None, + 'enforce': None, + 'values': [] + } diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf index 04ae4adf..13f8e335 100644 --- a/tests/modules/organization/fixture/main.tf +++ b/tests/modules/organization/fixture/main.tf @@ -28,8 +28,7 @@ module "test" { iam_audit_config = var.iam_audit_config logging_sinks = var.logging_sinks logging_exclusions = var.logging_exclusions - policy_boolean = var.policy_boolean - policy_list = var.policy_list + org_policies = var.org_policies tag_bindings = var.tag_bindings tags = var.tags } diff --git a/tests/modules/organization/fixture/variables.tf b/tests/modules/organization/fixture/variables.tf index 1d7ca88d..f56e51dc 100644 --- a/tests/modules/organization/fixture/variables.tf +++ b/tests/modules/organization/fixture/variables.tf @@ -44,16 +44,6 @@ variable "iam_audit_config" { default = {} } -variable "policy_boolean" { - type = any - default = {} -} - -variable "policy_list" { - type = any - default = {} -} - variable "firewall_policies" { type = any default = {} @@ -79,6 +69,11 @@ variable "logging_exclusions" { default = {} } +variable "org_policies" { + type = any + default = {} +} + variable "tag_bindings" { type = any default = null diff --git a/tests/modules/organization/test_plan.py b/tests/modules/organization/test_plan.py index a40758a2..37860ab6 100644 --- a/tests/modules/organization/test_plan.py +++ b/tests/modules/organization/test_plan.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_audit_config(plan_runner): "Test audit config." iam_audit_config = '{allServices={DATA_READ=[], DATA_WRITE=["user:me@example.org"]}}' _, resources = plan_runner(iam_audit_config=iam_audit_config) assert len(resources) == 1 - log_types = set(r['log_type'] - for r in resources[0]['values']['audit_log_config']) + log_types = set( + r['log_type'] for r in resources[0]['values']['audit_log_config']) assert log_types == set(['DATA_READ', 'DATA_WRITE']) @@ -28,21 +29,21 @@ def test_iam(plan_runner): '{' '"owners@example.org" = ["roles/owner", "roles/resourcemanager.folderAdmin"],' '"viewers@example.org" = ["roles/viewer"]' - '}' - ) - iam = ( - '{' - '"roles/owner" = ["user:one@example.org", "user:two@example.org"],' - '"roles/browser" = ["domain:example.org"]' - '}' - ) + '}') + iam = ('{' + '"roles/owner" = ["user:one@example.org", "user:two@example.org"],' + '"roles/browser" = ["domain:example.org"]' + '}') _, resources = plan_runner(group_iam=group_iam, iam=iam) roles = sorted([(r['values']['role'], sorted(r['values']['members'])) - for r in resources if r['type'] == 'google_organization_iam_binding']) + for r in resources + if r['type'] == 'google_organization_iam_binding']) assert roles == [ ('roles/browser', ['domain:example.org']), - ('roles/owner', ['group:owners@example.org', 'user:one@example.org', - 'user:two@example.org']), + ('roles/owner', [ + 'group:owners@example.org', 'user:one@example.org', + 'user:two@example.org' + ]), ('roles/resourcemanager.folderAdmin', ['group:owners@example.org']), ('roles/viewer', ['group:viewers@example.org']), ] @@ -50,55 +51,12 @@ def test_iam(plan_runner): def test_iam_additive_members(plan_runner): "Test IAM additive members." - iam = ( - '{"user:one@example.org" = ["roles/owner"],' - '"user:two@example.org" = ["roles/owner", "roles/editor"]}' - ) + iam = ('{"user:one@example.org" = ["roles/owner"],' + '"user:two@example.org" = ["roles/owner", "roles/editor"]}') _, resources = plan_runner(iam_additive_members=iam) roles = set((r['values']['role'], r['values']['member']) - for r in resources if r['type'] == 'google_organization_iam_member') - assert roles == set([ - ('roles/owner', 'user:one@example.org'), - ('roles/owner', 'user:two@example.org'), - ('roles/editor', 'user:two@example.org') - ]) - - -def test_policy_boolean(plan_runner): - "Test boolean org policy." - policy_boolean = '{policy-a = true, policy-b = false, policy-c = null}' - _, resources = plan_runner(policy_boolean=policy_boolean) - assert len(resources) == 3 - constraints = set(r['values']['constraint'] for r in resources) - assert set(constraints) == set(['policy-a', 'policy-b', 'policy-c']) - policies = [] - for resource in resources: - for policy in ('boolean_policy', 'restore_policy'): - value = resource['values'][policy] - if value: - policies.append((policy,) + value[0].popitem()) - assert set(policies) == set([ - ('boolean_policy', 'enforced', True), - ('boolean_policy', 'enforced', False), - ('restore_policy', 'default', True)]) - - -def test_policy_list(plan_runner): - "Test list org policy." - policy_list = ( - '{' - 'policy-a = {inherit_from_parent = true, suggested_value = null, status = true, values = []}, ' - 'policy-b = {inherit_from_parent = null, suggested_value = "foo", status = false, values = ["bar"]}, ' - 'policy-c = {inherit_from_parent = null, suggested_value = true, status = null, values = null}' - '}' - ) - _, resources = plan_runner(policy_list=policy_list) - assert len(resources) == 3 - values = [r['values'] for r in resources] - assert [r['constraint'] - for r in values] == ['policy-a', 'policy-b', 'policy-c'] - assert values[0]['list_policy'][0]['allow'] == [ - {'all': True, 'values': None}] - assert values[1]['list_policy'][0]['deny'] == [ - {'all': False, 'values': ["bar"]}] - assert values[2]['restore_policy'] == [{'default': True}] + for r in resources + if r['type'] == 'google_organization_iam_member') + assert roles == set([('roles/owner', 'user:one@example.org'), + ('roles/owner', 'user:two@example.org'), + ('roles/editor', 'user:two@example.org')]) diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py new file mode 100644 index 00000000..63ff2e76 --- /dev/null +++ b/tests/modules/organization/test_plan_org_policies.py @@ -0,0 +1,227 @@ +# Copyright 2022 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. + + +def test_policy_boolean(plan_runner): + "Test boolean org policy." + policies = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 2 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 2 + assert all( + x['values']['parent'] == 'organizations/1234567890' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyCreation' + ][0] + + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyUpload' + ][0] + + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert len(p2['rules']) == 2 + assert p2['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'FALSE', + 'values': [] + } + assert p2['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + } + + +def test_policy_list(plan_runner): + "Test list org policy." + policies = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 3 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 3 + assert all( + x['values']['parent'] == 'organizations/1234567890' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.vmExternalIpAccess' + ][0] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': 'TRUE', + 'enforce': None, + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.allowedPolicyMemberDomains' + ][0] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert p2['rules'] == [{ + 'allow_all': + None, + 'condition': [], + 'deny_all': + None, + 'enforce': + None, + 'values': [{ + 'allowed_values': [ + 'C0xxxxxxx', + 'C0yyyyyyy', + ], + 'denied_values': None + }] + }] + + p3 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' + ][0] + assert p3['inherit_from_parent'] is None + assert p3['reset'] is None + assert len(p3['rules']) == 3 + assert p3['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': None, + 'denied_values': ['in:EXTERNAL'] + }] + } + + assert p3['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': ['EXTERNAL_1'], + 'denied_values': None + }] + } + + assert p3['rules'][2] == { + 'allow_all': 'TRUE', + 'condition': [{ + 'description': + 'test condition2', + 'expression': + 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', + 'location': + 'xxx', + 'title': + 'condition2' + }], + 'deny_all': None, + 'enforce': None, + 'values': [] + } diff --git a/tests/modules/project/fixture/main.tf b/tests/modules/project/fixture/main.tf index a9867e5d..4c7441ac 100644 --- a/tests/modules/project/fixture/main.tf +++ b/tests/modules/project/fixture/main.tf @@ -25,12 +25,11 @@ module "test" { iam_additive_members = var.iam_additive_members labels = var.labels lien_reason = var.lien_reason + org_policies = var.org_policies oslogin = var.oslogin oslogin_admins = var.oslogin_admins oslogin_users = var.oslogin_users parent = var.parent - policy_boolean = var.policy_boolean - policy_list = var.policy_list prefix = var.prefix service_encryption_key_ids = var.service_encryption_key_ids services = var.services @@ -63,4 +62,3 @@ module "test-svpc-service" { } } } - diff --git a/tests/modules/project/fixture/variables.tf b/tests/modules/project/fixture/variables.tf index 2a4d95d1..236cb69f 100644 --- a/tests/modules/project/fixture/variables.tf +++ b/tests/modules/project/fixture/variables.tf @@ -64,6 +64,11 @@ variable "lien_reason" { default = "" } +variable "org_policies" { + type = any + default = {} +} + variable "oslogin" { type = bool default = false @@ -84,21 +89,6 @@ variable "parent" { default = null } -variable "policy_boolean" { - type = map(bool) - default = {} -} - -variable "policy_list" { - type = map(object({ - inherit_from_parent = bool - suggested_value = string - status = bool - values = list(string) - })) - default = {} -} - variable "prefix" { type = string default = null diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py index 645db0df..a9c4df68 100644 --- a/tests/modules/project/test_plan_org_policies.py +++ b/tests/modules/project/test_plan_org_policies.py @@ -12,47 +12,214 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_policy_boolean(plan_runner): "Test boolean org policy." - policy_boolean = '{policy-a = true, policy-b = false, policy-c = null}' - _, resources = plan_runner(policy_boolean=policy_boolean) - assert len(resources) == 7 - resources = [r for r in resources if r['type'] - == 'google_project_organization_policy'] - assert sorted([r['index'] for r in resources]) == [ - 'policy-a', 'policy-b', 'policy-c' - ] - policy_values = [] - for resource in resources: - for policy in ('boolean_policy', 'restore_policy'): - value = resource['values'][policy] - if value: - policy_values.append((policy,) + value[0].popitem()) - assert sorted(policy_values) == [ - ('boolean_policy', 'enforced', False), - ('boolean_policy', 'enforced', True), - ('restore_policy', 'default', True) - ] + policies = '''{ + "iam.disableServiceAccountKeyCreation" = { + enforce = true + } + "iam.disableServiceAccountKeyUpload" = { + enforce = false + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + enforce = true + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) + assert len(resources) == 6 + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 2 + assert all(x['values']['parent'] == 'projects/my-project' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyCreation' + ][0] + + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.disableServiceAccountKeyUpload' + ][0] + + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert len(p2['rules']) == 2 + assert p2['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': 'FALSE', + 'values': [] + } + assert p2['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': 'TRUE', + 'values': [] + } def test_policy_list(plan_runner): "Test list org policy." - policy_list = ( - '{' - 'policy-a = {inherit_from_parent = true, suggested_value = null, status = true, values = []}, ' - 'policy-b = {inherit_from_parent = null, suggested_value = "foo", status = false, values = ["bar"]}, ' - 'policy-c = {inherit_from_parent = null, suggested_value = true, status = null, values = null}' - '}' - ) - _, resources = plan_runner(policy_list=policy_list) + policies = '''{ + "compute.vmExternalIpAccess" = { + deny = { all = true } + } + "iam.allowedPolicyMemberDomains" = { + allow = { + values = ["C0xxxxxxx", "C0yyyyyyy"] + } + } + "compute.restrictLoadBalancerCreationForTypes" = { + deny = { values = ["in:EXTERNAL"] } + rules = [ + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/1234\\", \\"tagValues/1234\\")" + title = "condition" + description = "test condition" + location = "xxx" + } + allow = { + values = ["EXTERNAL_1"] + } + }, + { + condition = { + expression = "resource.matchTagId(\\"tagKeys/12345\\", \\"tagValues/12345\\")" + title = "condition2" + description = "test condition2" + location = "xxx" + } + allow = { + all = true + } + } + ] + } + }''' + _, resources = plan_runner(org_policies=policies) assert len(resources) == 7 - values = [r['values'] for r in resources if r['type'] - == 'google_project_organization_policy'] - assert [r['constraint'] for r in values] == [ - 'policy-a', 'policy-b', 'policy-c' - ] - assert values[0]['list_policy'][0]['allow'] == [ - {'all': True, 'values': None}] - assert values[1]['list_policy'][0]['deny'] == [ - {'all': False, 'values': ["bar"]}] - assert values[2]['restore_policy'] == [{'default': True}] + + policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] + assert len(policies) == 3 + assert all(x['values']['parent'] == 'projects/my-project' for x in policies) + + p1 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.vmExternalIpAccess' + ][0] + assert p1['inherit_from_parent'] is None + assert p1['reset'] is None + assert p1['rules'] == [{ + 'allow_all': None, + 'condition': [], + 'deny_all': 'TRUE', + 'enforce': None, + 'values': [] + }] + + p2 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'iam.allowedPolicyMemberDomains' + ][0] + assert p2['inherit_from_parent'] is None + assert p2['reset'] is None + assert p2['rules'] == [{ + 'allow_all': + None, + 'condition': [], + 'deny_all': + None, + 'enforce': + None, + 'values': [{ + 'allowed_values': [ + 'C0xxxxxxx', + 'C0yyyyyyy', + ], + 'denied_values': None + }] + }] + + p3 = [ + r['values']['spec'][0] + for r in policies + if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' + ][0] + assert p3['inherit_from_parent'] is None + assert p3['reset'] is None + assert len(p3['rules']) == 3 + assert p3['rules'][0] == { + 'allow_all': None, + 'condition': [], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': None, + 'denied_values': ['in:EXTERNAL'] + }] + } + + assert p3['rules'][1] == { + 'allow_all': None, + 'condition': [{ + 'description': 'test condition', + 'expression': 'resource.matchTagId("tagKeys/1234", "tagValues/1234")', + 'location': 'xxx', + 'title': 'condition' + }], + 'deny_all': None, + 'enforce': None, + 'values': [{ + 'allowed_values': ['EXTERNAL_1'], + 'denied_values': None + }] + } + + assert p3['rules'][2] == { + 'allow_all': 'TRUE', + 'condition': [{ + 'description': + 'test condition2', + 'expression': + 'resource.matchTagId("tagKeys/12345", "tagValues/12345")', + 'location': + 'xxx', + 'title': + 'condition2' + }], + 'deny_all': None, + 'enforce': None, + 'values': [] + }