diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 6de3081ce..840a941a5 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,8 +52,6 @@ locals { org_policies = { for k, v in local._org_policies : k => merge(v, { - name = "${local.folder_id}/policies/${k}" - parent = local.folder_id is_boolean_policy = ( alltrue([for r in v.rules : r.allow == null && r.deny == null]) ) @@ -75,37 +73,81 @@ locals { } resource "google_org_policy_policy" "default" { - for_each = local.org_policies - name = each.value.name - parent = each.value.parent - spec { - inherit_from_parent = each.value.inherit_from_parent - reset = each.value.reset - 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 - ) - dynamic "condition" { - for_each = rule.value.condition.expression != null ? [1] : [] - content { - description = rule.value.condition.description - expression = rule.value.condition.expression - location = rule.value.condition.location - title = rule.value.condition.title + for_each = toset([ + for k, v in local._org_policies : trimprefix(k, "dry_run:") + ]) + name = "${local.folder_id}/policies/${each.value}" + parent = local.folder_id + dynamic "spec" { + for_each = lookup(local.org_policies, each.value, null) != null ? [local.org_policies[each.value]] : [] + iterator = spec + content { + inherit_from_parent = spec.value.inherit_from_parent + reset = spec.value.reset + dynamic "rules" { + for_each = spec.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 = ( + spec.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + 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 "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 "dry_run_spec" { + for_each = lookup(local.org_policies, "dry_run:${each.value}", null) != null ? [local.org_policies["dry_run:${each.value}"]] : [] + iterator = spec + content { + inherit_from_parent = spec.value.inherit_from_parent + reset = spec.value.reset + dynamic "rules" { + for_each = spec.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 = ( + spec.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + 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) + } } } } diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index 2faf2e97e..55e3fc995 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,8 +52,6 @@ locals { org_policies = { for k, v in local._org_policies : k => merge(v, { - name = "${var.organization_id}/policies/${k}" - parent = var.organization_id is_boolean_policy = ( alltrue([for r in v.rules : r.allow == null && r.deny == null]) ) @@ -75,37 +73,81 @@ locals { } resource "google_org_policy_policy" "default" { - for_each = local.org_policies - name = each.value.name - parent = each.value.parent - spec { - inherit_from_parent = each.value.inherit_from_parent - reset = each.value.reset - 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 - ) - dynamic "condition" { - for_each = rule.value.condition.expression != null ? [1] : [] - content { - description = rule.value.condition.description - expression = rule.value.condition.expression - location = rule.value.condition.location - title = rule.value.condition.title + for_each = toset([ + for k, v in local._org_policies : trimprefix(k, "dry_run:") + ]) + name = "${var.organization_id}/policies/${each.value}" + parent = var.organization_id + dynamic "spec" { + for_each = lookup(local.org_policies, each.value, null) != null ? [local.org_policies[each.value]] : [] + iterator = spec + content { + inherit_from_parent = spec.value.inherit_from_parent + reset = spec.value.reset + dynamic "rules" { + for_each = spec.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 = ( + spec.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + 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 "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 "dry_run_spec" { + for_each = lookup(local.org_policies, "dry_run:${each.value}", null) != null ? [local.org_policies["dry_run:${each.value}"]] : [] + iterator = spec + content { + inherit_from_parent = spec.value.inherit_from_parent + reset = spec.value.reset + dynamic "rules" { + for_each = spec.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 = ( + spec.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + 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) + } } } } diff --git a/modules/project/README.md b/modules/project/README.md index 9a77bd26a..083e0915c 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -15,6 +15,7 @@ This module implements the creation and management of one GCP project including - [Shared VPC](#shared-vpc) - [Organization Policies](#organization-policies) - [Organization Policy Factory](#organization-policy-factory) + - [Dry-Run Mode](#dry-run-mode) - [Log Sinks](#log-sinks) - [Data Access Logs](#data-access-logs) - [Cloud KMS Encryption Keys](#cloud-kms-encryption-keys) @@ -590,6 +591,27 @@ iam.allowedPolicyMemberDomains: - C0yyyyyyy ``` +### Dry-Run Mode + +To enable dry-run mode, add the `dry_run:` prefix to the constraint name in your Terraform configuration: + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project" + parent = var.folder_id + org_policies = { + "gcp.restrictTLSVersion" = { + rules = [{ deny = { values = ["TLS_VERSION_1"] } }] + } + "dry_run:gcp.restrictTLSVersion" = { + rules = [{ deny = { values = ["TLS_VERSION_1", "TLS_VERSION_1_1"] } }] + } + } +} +# tftest modules=1 resources=2 inventory=org-policies-dry-run.yaml +``` + ## Log Sinks ```hcl diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index 32ebf66fc..ef6dfb887 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,8 +52,6 @@ locals { org_policies = { for k, v in local._org_policies : k => merge(v, { - name = "projects/${local.project.project_id}/policies/${k}" - parent = "projects/${local.project.project_id}" is_boolean_policy = ( alltrue([for r in v.rules : r.allow == null && r.deny == null]) ) @@ -75,37 +73,81 @@ locals { } resource "google_org_policy_policy" "default" { - for_each = local.org_policies - name = each.value.name - parent = each.value.parent - spec { - inherit_from_parent = each.value.inherit_from_parent - reset = each.value.reset - 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 - ) - dynamic "condition" { - for_each = rule.value.condition.expression != null ? [1] : [] - content { - description = rule.value.condition.description - expression = rule.value.condition.expression - location = rule.value.condition.location - title = rule.value.condition.title + for_each = toset([ + for k, v in local._org_policies : trimprefix(k, "dry_run:") + ]) + name = "projects/${local.project.project_id}/policies/${each.value}" + parent = "projects/${local.project.project_id}" + dynamic "spec" { + for_each = lookup(local.org_policies, each.value, null) != null ? [local.org_policies[each.value]] : [] + iterator = spec + content { + inherit_from_parent = spec.value.inherit_from_parent + reset = spec.value.reset + dynamic "rules" { + for_each = spec.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 = ( + spec.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + 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 "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 "dry_run_spec" { + for_each = lookup(local.org_policies, "dry_run:${each.value}", null) != null ? [local.org_policies["dry_run:${each.value}"]] : [] + iterator = spec + content { + inherit_from_parent = spec.value.inherit_from_parent + reset = spec.value.reset + dynamic "rules" { + for_each = spec.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 = ( + spec.value.is_boolean_policy && rule.value.enforce != null + ? upper(tostring(rule.value.enforce)) + : null + ) + dynamic "condition" { + for_each = rule.value.condition.expression != null ? [1] : [] + content { + 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) + } } } } diff --git a/tests/modules/organization/test_plan_org_policies_modules.py b/tests/modules/organization/test_plan_org_policies_modules.py index f8839fc25..b225ba1ec 100644 --- a/tests/modules/organization/test_plan_org_policies_modules.py +++ b/tests/modules/organization/test_plan_org_policies_modules.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,11 +34,11 @@ def test_policy_implementation(): '@@ -17 +17 @@\n', '-# tfdoc:file:description Project-level organization policies.\n', '+# tfdoc:file:description Folder-level organization policies.\n', - '@@ -55,2 +55,2 @@\n', - '- name = "projects/${local.project.project_id}/policies/${k}"\n', - '- parent = "projects/${local.project.project_id}"\n', - '+ name = "${local.folder_id}/policies/${k}"\n', - '+ parent = local.folder_id\n', + '@@ -79,2 +79,2 @@\n', + '- name = "projects/${local.project.project_id}/policies/${each.value}"\n', + '- parent = "projects/${local.project.project_id}"\n', + '+ name = "${local.folder_id}/policies/${each.value}"\n', + '+ parent = local.folder_id\n', ] diff2 = difflib.unified_diff(lines['folder'], lines['organization'], 'folder', @@ -49,12 +49,12 @@ def test_policy_implementation(): '@@ -17 +17 @@\n', '-# tfdoc:file:description Folder-level organization policies.\n', '+# tfdoc:file:description Organization-level organization policies.\n', - '@@ -55,2 +55,2 @@\n', - '- name = "${local.folder_id}/policies/${k}"\n', - '- parent = local.folder_id\n', - '+ name = "${var.organization_id}/policies/${k}"\n', - '+ parent = var.organization_id\n', - '@@ -113,0 +114,9 @@\n', + '@@ -79,2 +79,2 @@\n', + '- name = "${local.folder_id}/policies/${each.value}"\n', + '- parent = local.folder_id\n', + '+ name = "${var.organization_id}/policies/${each.value}"\n', + '+ parent = var.organization_id\n', + '@@ -155,0 +156,9 @@\n', '+ depends_on = [\n', '+ google_organization_iam_binding.authoritative,\n', '+ google_organization_iam_binding.bindings,\n', diff --git a/tests/modules/project/examples/org-policies-dry-run.yaml b/tests/modules/project/examples/org-policies-dry-run.yaml new file mode 100644 index 000000000..3804c0bbe --- /dev/null +++ b/tests/modules/project/examples/org-policies-dry-run.yaml @@ -0,0 +1,62 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_org_policy_policy.default["gcp.restrictTLSVersion"]: + dry_run_spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: null + denied_values: + - TLS_VERSION_1 + - TLS_VERSION_1_1 + name: projects/project/policies/gcp.restrictTLSVersion + parent: projects/project + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: null + denied_values: + - TLS_VERSION_1 + timeouts: null + module.project.google_project.project[0]: + auto_create_network: false + billing_account: null + folder_id: '1122334455' + labels: null + name: project + org_id: null + project_id: project + skip_delete: false + timeouts: null + +counts: + google_org_policy_policy: 1 + google_project: 1 + modules: 1 + resources: 2 + +outputs: {}