From 3562c52520156a80aa572a191350dd99d1387fe4 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 8 Nov 2022 09:34:38 +0100 Subject: [PATCH 1/8] Add support for org policy custom constraints --- modules/organization/README.md | 118 ++++++++++++++++-- modules/organization/organization-policies.tf | 46 +++++++ modules/organization/variables.tf | 31 ++++- 3 files changed, 182 insertions(+), 13 deletions(-) diff --git a/modules/organization/README.md b/modules/organization/README.md index 84a7da84..1c7a59b1 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -6,6 +6,7 @@ This module allows managing several organization properties: - custom IAM roles - audit logging configuration for services - organization policies +- organization policy custom constraints To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. @@ -22,7 +23,21 @@ module "org" { "roles/resourcemanager.projectCreator" = ["group:cloud-admins@example.org"] } + org_policy_custom_constraints = { + "custom.gkeEnableAutoUpgrade" = { + resource_types = ["container.googleapis.com/NodePool"] + method_types = ["CREATE"] + condition = "resource.management.autoUpgrade == true" + action_type = "ALLOW" + display_name = "Enable node auto-upgrade" + description = "All node pools must have node auto-upgrade enabled." + } + } + org_policies = { + "custom.gkeEnableAutoUpgrade" = { + enforce = true + } "compute.disableGuestAttributesAccess" = { enforce = true } @@ -61,7 +76,7 @@ module "org" { } } } -# tftest modules=1 resources=10 +# tftest modules=1 resources=12 ``` ## IAM @@ -74,15 +89,100 @@ There are several mutually exclusive ways of managing IAM in this module If you set audit policies via the `iam_audit_config_authoritative` variable, be sure to also configure IAM bindings via `iam_bindings_authoritative`, as audit policies use the underlying `google_organization_iam_policy` resource, which is also authoritative for any role. -Some care must also be takend with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph. +Some care must also be taken with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph. ### Organization policy factory See the [organization policy factory in the project module](../project#organization-policy-factory). +### Org policy custom constraints + +Refer to the [Creating and managing custom constraints](https://cloud.google.com/resource-manager/docs/organization-policy/creating-managing-custom-constraints) documentation for details on usage. +To manage organization policy custom constraints, the `orgpolicy.googleapis.com` service should be enabled in the quota project. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + + org_policy_custom_constraints = { + "custom.gkeEnableAutoUpgrade" = { + resource_types = ["container.googleapis.com/NodePool"] + method_types = ["CREATE"] + condition = "resource.management.autoUpgrade == true" + action_type = "ALLOW" + display_name = "Enable node auto-upgrade" + description = "All node pools must have node auto-upgrade enabled." + } + } + + # not necessarily to enforce on the org level, policy may be applied on folder/project levels + org_policies = { + "custom.gkeEnableAutoUpgrade" = { + enforce = true + } + } +} +# tftest modules=1 resources=2 +``` + +### Org policy custom constraints factory + +Org policy custom constraints can be loaded from a directory containing YAML files where each file defines one or more custom constraints. The structure of the YAML files is exactly the same as the `org_policy_custom_constraints` variable. + +The example below deploys a few org policy custom constraints split between two YAML files. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + + org_policy_custom_constraints_data_path = "/my/path" + +} +# tftest skip +``` + +```yaml +# /my/path/gke.yaml +custom.gkeEnableLogging: + resource_types: + - container.googleapis.com/Cluster + method_types: + - CREATE + - UPDATE + condition: resource.loggingService == "none" + action_type: DENY + display_name: Do not disable Cloud Logging +custom.gkeEnableAutoUpgrade: + resource_types: + - container.googleapis.com/NodePool + method_types: + - CREATE + condition: resource.management.autoUpgrade == true + action_type: ALLOW + display_name: Enable node auto-upgrade + description: All node pools must have node auto-upgrade enabled. +``` + +```yaml +# /my/path/dataproc.yaml + +custom.dataprocNoMoreThan10Workers + resource_types: + - dataproc.googleapis.com/Cluster + method_types: + - CREATE + - UPDATE + condition: resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10 + action_type: DENY + display_name: Total number of worker instances cannot be larger than 10 + description: Cluster cannot have more than 10 workers, including primary and secondary workers. +``` + ## Hierarchical firewall policies -Hirerarchical firewall policies can be managed in two ways: +Hierarchical firewall policies can be managed in two ways: - via the `firewall_policies` variable, to directly define policies and rules in Terraform - via the `firewall_policy_factory` variable, to leverage external YaML files via a simple "factory" embedded in the module ([see here](../../blueprints/factories) for more context on factories) @@ -314,7 +414,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_org_policy_policy | +| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_custom_constraint · 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. | | @@ -324,7 +424,7 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L191) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L217) | 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({…}))) | | {} | @@ -340,9 +440,11 @@ module "org" { | [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({…})) | | {} | | [org_policies](variables.tf#L151) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L200) | Path containing org policies in YAML format. | string | | null | -| [tag_bindings](variables.tf#L206) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L212) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | +| [org_policies_data_path](variables.tf#L191) | Path containing org policies in YAML format. | string | | null | +| [org_policy_custom_constraints](variables.tf#L197) | Organization policiy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policy_custom_constraints_data_path](variables.tf#L211) | Path containing org policy custom constraints in YAML format. | string | | null | +| [tag_bindings](variables.tf#L227) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L233) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | null | ## Outputs diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index fcde1658..4e82467b 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -88,6 +88,37 @@ locals { ] }) } + + _custom_constraints_factory_data_raw = ( + var.org_policy_custom_constraints_data_path == null + ? tomap({}) + : merge([ + for f in fileset(var.org_policy_custom_constraints_data_path, "*.yaml") : + yamldecode(file("${var.org_policy_custom_constraints_data_path}/${f}")) + ]...) + ) + + _custom_constraints_factory_data = { + for k, v in local._custom_constraints_factory_data_raw : + k => { + display_name = try(v.display_name, null) + description = try(v.description, null) + action_type = v.action_type + condition = v.condition + method_types = v.method_types + resource_types = v.resource_types + } + } + + _custom_constraints = merge(local._custom_constraints_factory_data, var.org_policy_custom_constraints) + + custom_constraints = { + for k, v in local._custom_constraints : + k => merge(v, { + name = k + parent = var.organization_id + }) + } } resource "google_org_policy_policy" "default" { @@ -150,5 +181,20 @@ resource "google_org_policy_policy" "default" { google_organization_iam_custom_role.roles, google_organization_iam_member.additive, google_organization_iam_policy.authoritative, + google_org_policy_custom_constraint.constraint, ] } + +resource "google_org_policy_custom_constraint" "constraint" { + provider = google-beta + + for_each = local.custom_constraints + name = each.value.name + parent = each.value.parent + display_name = each.value.display_name + description = each.value.description + action_type = each.value.action_type + condition = each.value.condition + method_types = each.value.method_types + resource_types = each.value.resource_types +} diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 7093a118..5b98a9e1 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -188,6 +188,32 @@ variable "org_policies" { nullable = false } +variable "org_policies_data_path" { + description = "Path containing org policies in YAML format." + type = string + default = null +} + +variable "org_policy_custom_constraints" { + description = "Organization policiy custom constraints keyed by constraint name." + type = map(object({ + display_name = optional(string) + description = optional(string) + action_type = string + condition = string + method_types = list(string) + resource_types = list(string) + })) + default = {} + nullable = false +} + +variable "org_policy_custom_constraints_data_path" { + description = "Path containing org policy custom constraints in YAML format." + type = string + default = null +} + variable "organization_id" { description = "Organization id in organizations/nnnnnn format." type = string @@ -197,11 +223,6 @@ variable "organization_id" { } } -variable "org_policies_data_path" { - description = "Path containing org policies in YAML format." - type = string - default = null -} variable "tag_bindings" { description = "Tag bindings for this organization, in key => tag value id format." From aae6ab132c456381cdcfdb671d3bac0184b11710 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 8 Nov 2022 18:10:13 +0100 Subject: [PATCH 2/8] Add tests for org policy custom constraints --- .../org-policy-custom-constraints.tf | 62 +++++++++++++++++++ modules/organization/organization-policies.tf | 45 -------------- tests/modules/organization/fixture/main.tf | 36 ++++++----- .../test.orgpolicy-custom-constraints.tfvars | 18 ++++++ .../modules/organization/fixture/variables.tf | 10 +++ .../organization/test_plan_org_policies.py | 16 ++++- .../test_plan_org_policies_modules.py | 8 +-- .../modules/organization/validate_policies.py | 22 +++++++ 8 files changed, 150 insertions(+), 67 deletions(-) create mode 100644 modules/organization/org-policy-custom-constraints.tf create mode 100644 tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars diff --git a/modules/organization/org-policy-custom-constraints.tf b/modules/organization/org-policy-custom-constraints.tf new file mode 100644 index 00000000..bfc2de1b --- /dev/null +++ b/modules/organization/org-policy-custom-constraints.tf @@ -0,0 +1,62 @@ +/** + * 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. + */ + +locals { + _custom_constraints_factory_data_raw = ( + var.org_policy_custom_constraints_data_path == null + ? tomap({}) + : tomap(merge([ + for f in fileset(var.org_policy_custom_constraints_data_path, "*.yaml") : + yamldecode(file("${var.org_policy_custom_constraints_data_path}/${f}")) + ]...)) + ) + + _custom_constraints_factory_data = { + for k, v in local._custom_constraints_factory_data_raw : + k => { + display_name = try(v.display_name, null) + description = try(v.description, null) + action_type = v.action_type + condition = v.condition + method_types = v.method_types + resource_types = v.resource_types + } + } + + _custom_constraints = merge(local._custom_constraints_factory_data, var.org_policy_custom_constraints) + + custom_constraints = { + for k, v in local._custom_constraints : + k => merge(v, { + name = k + parent = var.organization_id + }) + } +} + +resource "google_org_policy_custom_constraint" "constraint" { + provider = google-beta + + for_each = local.custom_constraints + name = each.value.name + parent = each.value.parent + display_name = each.value.display_name + description = each.value.description + action_type = each.value.action_type + condition = each.value.condition + method_types = each.value.method_types + resource_types = each.value.resource_types +} diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index 4e82467b..425e8f52 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -88,37 +88,6 @@ locals { ] }) } - - _custom_constraints_factory_data_raw = ( - var.org_policy_custom_constraints_data_path == null - ? tomap({}) - : merge([ - for f in fileset(var.org_policy_custom_constraints_data_path, "*.yaml") : - yamldecode(file("${var.org_policy_custom_constraints_data_path}/${f}")) - ]...) - ) - - _custom_constraints_factory_data = { - for k, v in local._custom_constraints_factory_data_raw : - k => { - display_name = try(v.display_name, null) - description = try(v.description, null) - action_type = v.action_type - condition = v.condition - method_types = v.method_types - resource_types = v.resource_types - } - } - - _custom_constraints = merge(local._custom_constraints_factory_data, var.org_policy_custom_constraints) - - custom_constraints = { - for k, v in local._custom_constraints : - k => merge(v, { - name = k - parent = var.organization_id - }) - } } resource "google_org_policy_policy" "default" { @@ -184,17 +153,3 @@ resource "google_org_policy_policy" "default" { google_org_policy_custom_constraint.constraint, ] } - -resource "google_org_policy_custom_constraint" "constraint" { - provider = google-beta - - for_each = local.custom_constraints - name = each.value.name - parent = each.value.parent - display_name = each.value.display_name - description = each.value.description - action_type = each.value.action_type - condition = each.value.condition - method_types = each.value.method_types - resource_types = each.value.resource_types -} diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf index 4f5df9e2..a620542e 100644 --- a/tests/modules/organization/fixture/main.tf +++ b/tests/modules/organization/fixture/main.tf @@ -15,21 +15,23 @@ */ module "test" { - source = "../../../../modules/organization" - organization_id = "organizations/1234567890" - custom_roles = var.custom_roles - firewall_policies = var.firewall_policies - firewall_policy_association = var.firewall_policy_association - firewall_policy_factory = var.firewall_policy_factory - group_iam = var.group_iam - iam = var.iam - iam_additive = var.iam_additive - iam_additive_members = var.iam_additive_members - iam_audit_config = var.iam_audit_config - logging_sinks = var.logging_sinks - logging_exclusions = var.logging_exclusions - org_policies = var.org_policies - org_policies_data_path = var.org_policies_data_path - tag_bindings = var.tag_bindings - tags = var.tags + source = "../../../../modules/organization" + organization_id = "organizations/1234567890" + custom_roles = var.custom_roles + firewall_policies = var.firewall_policies + firewall_policy_association = var.firewall_policy_association + firewall_policy_factory = var.firewall_policy_factory + group_iam = var.group_iam + iam = var.iam + iam_additive = var.iam_additive + iam_additive_members = var.iam_additive_members + iam_audit_config = var.iam_audit_config + logging_sinks = var.logging_sinks + logging_exclusions = var.logging_exclusions + org_policies = var.org_policies + org_policies_data_path = var.org_policies_data_path + org_policy_custom_constraints = var.org_policy_custom_constraints + org_policy_custom_constraints_data_path = var.org_policy_custom_constraints_data_path + tag_bindings = var.tag_bindings + tags = var.tags } diff --git a/tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars b/tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars new file mode 100644 index 00000000..a02f97c4 --- /dev/null +++ b/tests/modules/organization/fixture/test.orgpolicy-custom-constraints.tfvars @@ -0,0 +1,18 @@ +org_policy_custom_constraints = { + "custom.gkeEnableAutoUpgrade" = { + resource_types = ["container.googleapis.com/NodePool"] + method_types = ["CREATE"] + condition = "resource.management.autoUpgrade == true" + action_type = "ALLOW" + display_name = "Enable node auto-upgrade" + description = "All node pools must have node auto-upgrade enabled." + }, + "custom.dataprocNoMoreThan10Workers" = { + resource_types = ["dataproc.googleapis.com/Cluster"] + method_types = ["CREATE", "UPDATE"] + condition = "resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10" + action_type = "DENY" + display_name = "Total number of worker instances cannot be larger than 10" + description = "Cluster cannot have more than 10 workers, including primary and secondary workers." + } +} diff --git a/tests/modules/organization/fixture/variables.tf b/tests/modules/organization/fixture/variables.tf index 2508fb06..c4efa8fb 100644 --- a/tests/modules/organization/fixture/variables.tf +++ b/tests/modules/organization/fixture/variables.tf @@ -79,6 +79,16 @@ variable "org_policies_data_path" { default = null } +variable "org_policy_custom_constraints" { + type = any + default = {} +} + +variable "org_policy_custom_constraints_data_path" { + type = any + default = null +} + variable "tag_bindings" { type = any default = null diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py index 8267106f..05550832 100644 --- a/tests/modules/organization/test_plan_org_policies.py +++ b/tests/modules/organization/test_plan_org_policies.py @@ -14,7 +14,7 @@ import pathlib -from .validate_policies import validate_policy_boolean, validate_policy_list +from .validate_policies import validate_policy_boolean, validate_policy_list, validate_policy_custom_constraints def test_policy_boolean(plan_runner): @@ -31,6 +31,13 @@ def test_policy_list(plan_runner): validate_policy_list(resources) +def test_policy_custom_constraints(plan_runner): + "Test org policy custom constraints." + tfvars = 'test.orgpolicy-custom-constraints.tfvars' + _, resources = plan_runner(tf_var_file=tfvars) + validate_policy_custom_constraints(resources) + + def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path): dest = tmp_path / 'policies.yaml' tfvars_to_yaml('test.orgpolicies-boolean.tfvars', dest, 'org_policies') @@ -43,3 +50,10 @@ def test_factory_policy_list(plan_runner, tfvars_to_yaml, tmp_path): tfvars_to_yaml('test.orgpolicies-list.tfvars', dest, 'org_policies') _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') validate_policy_list(resources) + + +def test_factory_policy_custom_constraints(plan_runner, tfvars_to_yaml, tmp_path): + dest = tmp_path / 'constraints.yaml' + tfvars_to_yaml('test.orgpolicy-custom-constraints.tfvars', dest, 'org_policy_custom_constraints') + _, resources = plan_runner(org_policy_custom_constraints_data_path=f'"{tmp_path}"') + validate_policy_custom_constraints(resources) diff --git a/tests/modules/organization/test_plan_org_policies_modules.py b/tests/modules/organization/test_plan_org_policies_modules.py index 81030035..d2a5e097 100644 --- a/tests/modules/organization/test_plan_org_policies_modules.py +++ b/tests/modules/organization/test_plan_org_policies_modules.py @@ -72,11 +72,10 @@ def test_policy_implementation(): '- name = "${local.folder.name}/policies/${k}"\n', '- parent = local.folder.name\n', '+ name = "${var.organization_id}/policies/${k}"\n', - '+ parent = var.organization_id\n', - ' \n', + '+ parent = var.organization_id\n', ' \n', ' is_boolean_policy = v.allow == null && v.deny == null\n', ' has_values = (\n', - '@@ -143,4 +143,12 @@\n', + '@@ -143,4 +143,13 @@\n', ' }\n', ' }\n', ' }\n', @@ -87,6 +86,7 @@ def test_policy_implementation(): '+ google_organization_iam_custom_role.roles,\n', '+ google_organization_iam_member.additive,\n', '+ google_organization_iam_policy.authoritative,\n', + '+ google_org_policy_custom_constraint.constraint,\n', '+ ]\n', - ' }\n', + ' }\n' ] diff --git a/tests/modules/organization/validate_policies.py b/tests/modules/organization/validate_policies.py index 73acb088..51844b15 100644 --- a/tests/modules/organization/validate_policies.py +++ b/tests/modules/organization/validate_policies.py @@ -138,3 +138,25 @@ def validate_policy_list(resources): 'enforce': None, 'values': [] } + +def validate_policy_custom_constraints(resources): + assert len(resources) == 2 + assert all( + r['values']['parent'] == 'organizations/1234567890' for r in resources) + constraints = { + r['index']: r['values'] + for r in resources + if r['type'] == 'google_org_policy_custom_constraint' + } + assert len(constraints) == 2 + c1 = constraints['custom.gkeEnableAutoUpgrade'] + assert c1['resource_types'][0] == 'container.googleapis.com/NodePool' + assert c1['method_types'] == ['CREATE'] + assert c1['condition'] == 'resource.management.autoUpgrade == true' + assert c1['action_type'] == 'ALLOW' + + c2 = constraints['custom.dataprocNoMoreThan10Workers'] + assert c2['resource_types'][0] == 'dataproc.googleapis.com/Cluster' + assert c2['method_types'] == ['CREATE', 'UPDATE'] + assert c2['condition'] == 'resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10' + assert c2['action_type'] == 'DENY' From 02ca116260693a116140abba722bd856b2131b95 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Tue, 8 Nov 2022 09:38:15 +0100 Subject: [PATCH 3/8] FAST: bootstrap and extra stage CI/CD improvements and fixes (#956) * add clone commands output * always create secret key for repos, fix module source * optional modules ref * tfdoc * create secrets in the right repositories * add publick key to modules repository * bump Terraform version in CI templates * add template to populated files * tfdoc * do not error out writing ci/cd workflows when output files are disabled * update README * fix apply file outputs when outputs_location is changed to null --- fast/assets/templates/workflow-github.yaml | 2 +- .../assets/templates/workflow-sourcerepo.yaml | 2 +- fast/extras/00-cicd-github/README.md | 23 +++++++--- fast/extras/00-cicd-github/main.tf | 45 +++++++++++++------ fast/extras/00-cicd-github/outputs.tf | 23 ++++++++++ fast/extras/00-cicd-github/variables.tf | 6 +++ fast/stages/00-bootstrap/outputs-files.tf | 14 +++--- 7 files changed, 87 insertions(+), 28 deletions(-) create mode 100644 fast/extras/00-cicd-github/outputs.tf diff --git a/fast/assets/templates/workflow-github.yaml b/fast/assets/templates/workflow-github.yaml index 81b5f3d2..1efb9c66 100644 --- a/fast/assets/templates/workflow-github.yaml +++ b/fast/assets/templates/workflow-github.yaml @@ -30,7 +30,7 @@ env: SSH_AUTH_SOCK: /tmp/ssh_agent.sock TF_PROVIDERS_FILE: ${tf_providers_file} TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} - TF_VERSION: 1.1.7 + TF_VERSION: 1.3.2 jobs: fast-pr: diff --git a/fast/assets/templates/workflow-sourcerepo.yaml b/fast/assets/templates/workflow-sourcerepo.yaml index 7f6f08ff..446c9c96 100644 --- a/fast/assets/templates/workflow-sourcerepo.yaml +++ b/fast/assets/templates/workflow-sourcerepo.yaml @@ -95,4 +95,4 @@ substitutions: _FAST_OUTPUTS_BUCKET: ${outputs_bucket} _TF_PROVIDERS_FILE: ${tf_providers_file} _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} - _TF_VERSION: 1.1.7 + _TF_VERSION: 1.3.2 diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md index 2db9952c..52c322a3 100644 --- a/fast/extras/00-cicd-github/README.md +++ b/fast/extras/00-cicd-github/README.md @@ -8,11 +8,16 @@ This stage is designed for quick repository creation in a GitHub organization, a Initial file population of repositories is controlled via the `populate_from` attribute, and needs a bit of care: -- never run this stage gain with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. -- be mindful when enabling initial population of the modules repository, as the number of resulting files to manage is very close to the GitHub hourly limit for their API +- never run this stage with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. +- initial population of the modules repository is discouraged, as the number of resulting files Terraform needs to manage is very close to the GitHub hourly limit for their API, it's much easier to populate modules via regular git commands The scenario for which this stage has been designed is one-shot creation and/or population of stage repositories, running it multiple times with different variables and Terraform states if incremental creation is needed for subsequent FAST stages (e.g. GKE, data platform, etc.). +Once initial population is done, you need to manually push to the repository + +- the `.tfvars` file with custom variable values for your stages +- the workflow configuration file generated by FAST stages + ## GitHub provider credentials A [GitHub token](https://github.com/settings/tokens) is needed to authenticate against their API. The token needs organization-level permissions, like shown in this screenshot: @@ -77,7 +82,8 @@ When initial population is configured for a repository, this stage also adds a s | name | description | resources | |---|---|---| | [cicd-versions.tf](./cicd-versions.tf) | Provider version. | | -| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_file · tls_private_key | +| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_deploy_key · github_repository_file · tls_private_key | +| [outputs.tf](./outputs.tf) | Module outputs. | | | [providers.tf](./providers.tf) | Provider configuration. | | | [variables.tf](./variables.tf) | Module variables. | | @@ -85,8 +91,15 @@ When initial population is configured for a repository, this stage also adds a s | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization](variables.tf#L28) | GitHub organization. | string | ✓ | | +| [organization](variables.tf#L34) | GitHub organization. | string | ✓ | | | [commmit_config](variables.tf#L17) | Configure commit metadata. | object({…}) | | {} | -| [repositories](variables.tf#L33) | Repositories to create. | map(object({…})) | | {} | +| [modules_ref](variables.tf#L28) | Optional git ref used in module sources. | string | | null | +| [repositories](variables.tf#L39) | Repositories to create. | map(object({…})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [clone](outputs.tf#L17) | Clone repository commands. | | diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/00-cicd-github/main.tf index 140230d0..4ba49f73 100644 --- a/fast/extras/00-cicd-github/main.tf +++ b/fast/extras/00-cicd-github/main.tf @@ -30,6 +30,7 @@ locals { } ] if v.populate_from != null ]) + modules_ref = var.modules_ref == null ? "" : "?ref=${var.modules_ref}" modules_repository = ( length(local._modules_repository) > 0 ? local._modules_repository.0 @@ -39,13 +40,24 @@ locals { for k, v in var.repositories : k => v.create_options == null ? k : github_repository.default[k].name } - repository_files = { - for k in local._repository_files : - "${k.repository}/${k.name}" => k - if !endswith(k.name, ".tf") || ( - !startswith(k.name, "0") && k.name != "globals.tf" - ) - } + repository_files = merge( + { + for k in local._repository_files : + "${k.repository}/${k.name}" => k + if !endswith(k.name, ".tf") || ( + !startswith(k.name, "0") && k.name != "globals.tf" + ) + }, + { + for k, v in var.repositories : + "${k}/templates/providers.tf.tpl" => { + repository = k + file = "../../assets/templates/providers.tf.tpl" + name = "templates/providers.tf.tpl" + } + if v.populate_from != null + } + ) } resource "github_repository" "default" { @@ -88,15 +100,20 @@ resource "tls_private_key" "default" { algorithm = "ED25519" } +resource "github_repository_deploy_key" "exdefaultample_repository_deploy_key" { + count = local.modules_repository == null ? 0 : 1 + title = "Modules repository access" + repository = local.modules_repository + key = tls_private_key.default.0.public_key_openssh + read_only = true +} + resource "github_actions_secret" "default" { for_each = local.modules_repository == null ? {} : { for k, v in local.repositories : - k => v if( - k != local.modules_repository && - var.repositories[k].populate_from != null - ) + k => v if k != local.modules_repository } - repository = local.repositories[local.modules_repository] + repository = each.key secret_name = "CICD_MODULES_KEY" plaintext_value = tls_private_key.default.0.private_key_openssh } @@ -112,8 +129,8 @@ resource "github_repository_file" "default" { endswith(each.value.name, ".tf") && local.modules_repository != null ? replace( file(each.value.file), - "/source\\s*=\\s*\"../../../", - "source = \"git@github.com:${var.organization}/${local.modules_repository}.git/" + "/source\\s*=\\s*\"../../../modules/([^/\"]+)\"/", + "source = \"git@github.com:${var.organization}/${local.modules_repository}.git//$1${local.modules_ref}\"" # " ) : file(each.value.file) ) diff --git a/fast/extras/00-cicd-github/outputs.tf b/fast/extras/00-cicd-github/outputs.tf new file mode 100644 index 00000000..cb580e1f --- /dev/null +++ b/fast/extras/00-cicd-github/outputs.tf @@ -0,0 +1,23 @@ +/** + * 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. + */ + +output "clone" { + description = "Clone repository commands." + value = { + for k, v in var.repositories : + k => "git clone git@github.com:${var.organization}/${k}.git" + } +} diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/00-cicd-github/variables.tf index 29b79225..0d9cb7fd 100644 --- a/fast/extras/00-cicd-github/variables.tf +++ b/fast/extras/00-cicd-github/variables.tf @@ -25,6 +25,12 @@ variable "commmit_config" { nullable = false } +variable "modules_ref" { + description = "Optional git ref used in module sources." + type = string + default = null +} + variable "organization" { description = "GitHub organization." type = string diff --git a/fast/stages/00-bootstrap/outputs-files.tf b/fast/stages/00-bootstrap/outputs-files.tf index 3016c8e2..ded88cd5 100644 --- a/fast/stages/00-bootstrap/outputs-files.tf +++ b/fast/stages/00-bootstrap/outputs-files.tf @@ -19,27 +19,27 @@ resource "local_file" "providers" { for_each = var.outputs_location == null ? {} : local.providers file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/providers/${each.key}-providers.tf" - content = each.value + filename = "${try(pathexpand(var.outputs_location), "")}/providers/${each.key}-providers.tf" + content = try(each.value, null) } resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/00-bootstrap.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/00-bootstrap.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "local_file" "tfvars_globals" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/globals.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/globals.auto.tfvars.json" content = jsonencode(local.tfvars_globals) } resource "local_file" "workflows" { - for_each = local.cicd_workflows + for_each = var.outputs_location == null ? {} : local.cicd_workflows file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/workflows/${each.key}-workflow.yaml" - content = each.value + filename = "${try(pathexpand(var.outputs_location), "")}/workflows/${each.key}-workflow.yaml" + content = try(each.value, null) } From 1419a041471eebbcbe572ed4949374b55231e880 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 8 Nov 2022 18:17:05 +0100 Subject: [PATCH 4/8] Update module readme --- modules/organization/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/organization/README.md b/modules/organization/README.md index 1c7a59b1..95c4e2e8 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -414,7 +414,8 @@ 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_org_policy_custom_constraint · google_org_policy_policy | +| [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | google_org_policy_custom_constraint | +| [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. | | From 8282b6c0e2e8a4ab19e98e5153b2751742366030 Mon Sep 17 00:00:00 2001 From: Valerio Ponza <81154450+valeriobponza@users.noreply.github.com> Date: Wed, 9 Nov 2022 00:25:34 +0100 Subject: [PATCH 5/8] Fix README typo in firewall module (#960) * fixing readme in firewall module * fix typo Co-authored-by: Valerio Ponza Co-authored-by: Ludovico Magnocavallo --- modules/net-vpc-firewall/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/net-vpc-firewall/README.md b/modules/net-vpc-firewall/README.md index 7cc0d452..6b36edd9 100644 --- a/modules/net-vpc-firewall/README.md +++ b/modules/net-vpc-firewall/README.md @@ -138,10 +138,10 @@ module "firewall" { project_id = "my-project" network = "my-network" factories_config = { - + rules_folder = "config/firewall" + cidr_tpl_file = "config/cidr_template.yaml" } - data_folder = "config/firewall" - cidr_template_file = "config/cidr_template.yaml" + } # tftest skip ``` From ae4e0f9d44eb5b4c272cddba6539ad1d45e91c7f Mon Sep 17 00:00:00 2001 From: Ludo Date: Wed, 9 Nov 2022 07:58:39 +0100 Subject: [PATCH 6/8] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5989fde..6166ad9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ All notable changes to this project will be documented in this file. ### FAST +- [[#956](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/956)] FAST: bootstrap and extra stage CI/CD improvements and fixes ([ludoo](https://github.com/ludoo)) - [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) - [[#943](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/943)] Update bootstrap README.md with unique project id requirements ([KPRepos](https://github.com/KPRepos)) - [[#948](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/948)] Use display_name instead of description for FAST service accounts ([juliocc](https://github.com/juliocc)) @@ -90,6 +91,7 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#960](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/960)] Fix README typo in firewall module ([valeriobponza](https://github.com/valeriobponza)) - [[#953](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/953)] Added IAM Additive and converted some outputs to static ([muresan](https://github.com/muresan)) - [[#951](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/951)] cloud-functions v2 - fix reference to bucket_name ([wiktorn](https://github.com/wiktorn)) - [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) From 1b2fcb803c1add47dfed952ddc3a7ab16e1fff62 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 9 Nov 2022 08:53:11 +0100 Subject: [PATCH 7/8] remove extra file (#961) --- stages.png | Bin 39873 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 stages.png diff --git a/stages.png b/stages.png deleted file mode 100644 index 83f3c7e8e3d65f48a29ad563e65dfff246ea405c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39873 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 From 2bded25f359f89145a810af0bd55f087b3152464 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Wed, 9 Nov 2022 11:17:46 +0100 Subject: [PATCH 8/8] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6166ad9f..1d058a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ All notable changes to this project will be documented in this file. ### DOCUMENTATION +- [[#961](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/961)] Remove extra file from root ([ludoo](https://github.com/ludoo)) - [[#943](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/943)] Update bootstrap README.md with unique project id requirements ([KPRepos](https://github.com/KPRepos)) - [[#937](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/937)] Fix typos in blueprints README.md ([kumar-dhanagopal](https://github.com/kumar-dhanagopal)) - [[#921](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/921)] Align documentation, move glb blueprint ([ludoo](https://github.com/ludoo)) @@ -91,6 +92,7 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#958](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/958)] Add support for org policy custom constraints ([averbuks](https://github.com/averbuks)) - [[#960](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/960)] Fix README typo in firewall module ([valeriobponza](https://github.com/valeriobponza)) - [[#953](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/953)] Added IAM Additive and converted some outputs to static ([muresan](https://github.com/muresan)) - [[#951](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/951)] cloud-functions v2 - fix reference to bucket_name ([wiktorn](https://github.com/wiktorn))