Refactor project module, support per-file tags in tfdoc (#450)

* add support for in-doc tfdoc options overrides

* clean up project module

* add file description tags

* only output module and resource columns in tfdoc file table if they exist

* update fast READMEs

* fix check docs
This commit is contained in:
Ludovico Magnocavallo 2022-01-22 11:34:18 +01:00 committed by GitHub
parent 19c6e54298
commit 9a533180a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 544 additions and 428 deletions

View File

@ -55,12 +55,7 @@ jobs:
- name: Check documentation (fabric)
id: documentation-fabric
run: |
python3 tools/check_documentation.py examples modules
- name: Check documentation (fast)
id: documentation-fast
run: |
python3 tools/check_documentation.py --files --show-extra fast
python3 tools/check_documentation.py examples modules fast
# markdown-link-check:
# runs-on: ubuntu-latest

View File

@ -264,8 +264,7 @@ If a different convention is needed, identify names via search/grep (e.g. with `
Names used in internal references (e.g. `module.foo-prod.id`) are only used by Terraform and do not influence resource naming, so they are best left untouched to avoid having to debug complex errors.
<!-- TFDOC OPTS files:1 show_extra:1 -->
<!-- BEGIN TFDOC -->
## Files
@ -304,9 +303,3 @@ Names used in internal references (e.g. `module.foo-prod.id`) are only used by T
| tfvars | Terraform variable files for the following stages. | ✓ | |
<!-- END TFDOC -->

View File

@ -138,10 +138,7 @@ IAM roles can be easily edited in the relevant `branch-xxx.tf` file, following t
Due to its simplicity, this stage lends itself easily to customizations: adding a new top-level branch (e.g. for shared GKE clusters) is as easy as cloning one of the `branch-xxx.tf` files, and changing names.
<!-- TFDOC OPTS files:1 show_extra:1 -->
<!-- BEGIN TFDOC -->
## Files
@ -185,12 +182,3 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
| tfvars | Terraform variable files for the following stages. | ✓ | |
<!-- END TFDOC -->

View File

@ -282,6 +282,7 @@ VPN configuration also controls BGP advertisements, which requires the following
DNS configurations are centralised in the `dns.tf` file. Spokes delegate DNS resolution to Landing through DNS peering, and optionally define a private zone (e.g. `staging.gcp.example.com`) which the landing peers to. To configure DNS for a new environment, copy all the `prod-*` modules in the `dns.tf` file to `staging-*`, and update their content accordingly. Don't forget to add a peering zone from Landing to the newly created environment private zone.
<!-- TFDOC OPTS files:1 show_extra:1 -->
<!-- BEGIN TFDOC -->
## Files

View File

@ -265,9 +265,7 @@ Some references that might be useful in setting up this stage:
- [VPC SC CSCC requirements](https://cloud.google.com/security-command-center/docs/troubleshooting).
<!-- TFDOC OPTS files:1 show_extra:1 -->
<!-- BEGIN TFDOC -->
## Files
@ -309,15 +307,3 @@ Some references that might be useful in setting up this stage:
| stage_perimeter_projects | Security project numbers. They can be added to perimeter resources. | | |
<!-- END TFDOC -->

View File

@ -94,17 +94,16 @@ terraform init
terraform apply
```
<!-- TFDOC OPTS files:1 show_extra:1 -->
<!-- BEGIN TFDOC -->
## Files
| name | description | modules | resources |
|---|---|---|---|
| [main.tf](./main.tf) | Project factory. | <code>project-factory</code> | |
| [outputs.tf](./outputs.tf) | Module outputs. | | |
| [variables.tf](./variables.tf) | Module variables. | | |
| name | description | modules |
|---|---|---|
| [main.tf](./main.tf) | Project factory. | <code>project-factory</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [variables.tf](./variables.tf) | Module variables. | |
## Variables
@ -124,8 +123,3 @@ terraform apply
| projects | Created projects and service accounts. | | |
<!-- END TFDOC -->

View File

@ -150,6 +150,7 @@ module "project-host" {
```
## Cloud KMS encryption keys
```hcl
module "project" {
source = "./modules/project"
@ -173,9 +174,24 @@ module "project" {
# tftest:modules=1:resources=7
```
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->
## Files
| name | description | resources |
|---|---|---|
| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | <code>google_project_iam_binding</code> · <code>google_project_iam_custom_role</code> · <code>google_project_iam_member</code> |
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | <code>google_bigquery_dataset_iam_member</code> · <code>google_logging_project_exclusion</code> · <code>google_logging_project_sink</code> · <code>google_project_iam_member</code> · <code>google_pubsub_topic_iam_member</code> · <code>google_storage_bucket_iam_member</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_compute_project_metadata_item</code> · <code>google_essential_contacts_contact</code> · <code>google_monitoring_monitored_project</code> · <code>google_project</code> · <code>google_project_service</code> · <code>google_resource_manager_lien</code> |
| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | <code>google_project_organization_policy</code> |
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [service_accounts.tf](./service_accounts.tf) | Service identities and supporting resources. | <code>google_kms_crypto_key_iam_member</code> · <code>google_project_service_identity</code> |
| [shared_vpc.tf](./shared_vpc.tf) | Shared VPC project-level configuration. | <code>google_compute_shared_vpc_host_project</code> · <code>google_compute_shared_vpc_service_project</code> |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
| [vpc-sc.tf](./vpc-sc.tf) | VPC-SC project-level perimeter configuration. | <code>google_access_context_manager_service_perimeter_resource</code> |
## Variables
| name | description | type | required | default |
@ -224,5 +240,3 @@ module "project" {
| sink_writer_identities | Writer identities created for each sink. | |
<!-- END TFDOC -->

115
modules/project/iam.tf Normal file
View File

@ -0,0 +1,115 @@
/**
* 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 Generic and OSLogin-specific IAM bindings and roles.
# IAM notes:
# - external users need to have accepted the invitation email to join
# - oslogin roles also require role to list instances
# - additive (non-authoritative) roles might fail due to dynamic values
locals {
_group_iam_roles = distinct(flatten(values(var.group_iam)))
_group_iam = {
for r in local._group_iam_roles : r => [
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
_iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
_iam_additive_member_pairs = flatten([
for member, roles in var.iam_additive_members : [
for role in roles : { role = role, member = member }
]
])
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
try(var.iam[role], []),
try(local._group_iam[role], [])
)
}
iam_additive = {
for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) :
"${pair.role}-${pair.member}" => pair
}
}
resource "google_project_iam_custom_role" "roles" {
for_each = var.custom_roles
project = local.project.project_id
role_id = each.key
title = "Custom role ${each.key}"
description = "Terraform-managed"
permissions = each.value
}
resource "google_project_iam_binding" "authoritative" {
for_each = local.iam
project = local.project.project_id
role = each.key
members = each.value
depends_on = [
google_project_service.project_services,
google_project_iam_custom_role.roles
]
}
resource "google_project_iam_member" "additive" {
for_each = (
length(var.iam_additive) + length(var.iam_additive_members) > 0
? local.iam_additive
: {}
)
project = local.project.project_id
role = each.value.role
member = each.value.member
depends_on = [
google_project_service.project_services,
google_project_iam_custom_role.roles
]
}
resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" {
for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([])
project = local.project.project_id
role = "roles/iam.serviceAccountUser"
member = each.value
}
resource "google_project_iam_member" "oslogin_compute_viewer" {
for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([])
project = local.project.project_id
role = "roles/compute.viewer"
member = each.value
}
resource "google_project_iam_member" "oslogin_admins" {
for_each = var.oslogin ? toset(var.oslogin_admins) : toset([])
project = local.project.project_id
role = "roles/compute.osAdminLogin"
member = each.value
}
resource "google_project_iam_member" "oslogin_users" {
for_each = var.oslogin ? toset(var.oslogin_users) : toset([])
project = local.project.project_id
role = "roles/compute.osLogin"
member = each.value
}

View File

@ -0,0 +1,98 @@
/**
* 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 Log sinks and supporting resources.
locals {
logging_sinks = coalesce(var.logging_sinks, {})
sink_bindings = {
for type in ["gcs", "bigquery", "pubsub", "logging"] :
type => {
for name, sink in local.logging_sinks :
name => sink if sink.iam && sink.type == type
}
}
sink_type_destination = {
gcs = "storage.googleapis.com"
bigquery = "bigquery.googleapis.com"
pubsub = "pubsub.googleapis.com"
logging = "logging.googleapis.com"
}
}
resource "google_logging_project_sink" "sink" {
for_each = local.logging_sinks
name = each.key
#description = "${each.key} (Terraform-managed)"
project = local.project.project_id
destination = "${local.sink_type_destination[each.value.type]}/${each.value.destination}"
filter = each.value.filter
unique_writer_identity = each.value.unique_writer
dynamic "exclusions" {
for_each = each.value.exclusions
iterator = exclusion
content {
name = exclusion.key
filter = exclusion.value
}
}
depends_on = [
google_project_iam_binding.authoritative,
google_project_iam_member.additive
]
}
resource "google_storage_bucket_iam_member" "gcs-sinks-binding" {
for_each = local.sink_bindings["gcs"]
bucket = each.value.destination
role = "roles/storage.objectCreator"
member = google_logging_project_sink.sink[each.key].writer_identity
}
resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" {
for_each = local.sink_bindings["bigquery"]
project = split("/", each.value.destination)[1]
dataset_id = split("/", each.value.destination)[3]
role = "roles/bigquery.dataEditor"
member = google_logging_project_sink.sink[each.key].writer_identity
}
resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" {
for_each = local.sink_bindings["pubsub"]
project = split("/", each.value.destination)[1]
topic = split("/", each.value.destination)[3]
role = "roles/pubsub.publisher"
member = google_logging_project_sink.sink[each.key].writer_identity
}
resource "google_project_iam_member" "bucket-sinks-binding" {
for_each = local.sink_bindings["logging"]
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_project_sink.sink[each.key].writer_identity
# TODO(jccb): use a condition to limit writer-identity only to this
# bucket
}
resource "google_logging_project_exclusion" "logging-exclusion" {
for_each = coalesce(var.logging_exclusions, {})
name = each.key
project = local.project.project_id
description = "${each.key} (Terraform-managed)"
filter = each.value
}

View File

@ -15,44 +15,9 @@
*/
locals {
# service dependendencies for encryption keys
_service_encryption_key_dependent_services = {
"composer" : [
"composer",
"artifactregistry", "container-engine", "compute", "pubsub", "storage"
]
"dataflow" : ["dataflow", "compute"]
}
descriptive_name = (
var.descriptive_name != null ? var.descriptive_name : "${local.prefix}${var.name}"
)
group_iam_roles = distinct(flatten(values(var.group_iam)))
group_iam = {
for r in local.group_iam_roles : r => [
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
iam = {
for role in distinct(concat(keys(var.iam), keys(local.group_iam))) :
role => concat(
try(var.iam[role], []),
try(local.group_iam[role], [])
)
}
iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
iam_additive_member_pairs = flatten([
for member, roles in var.iam_additive_members : [
for role in roles : { role = role, member = member }
]
])
iam_additive = {
for pair in concat(local.iam_additive_pairs, local.iam_additive_member_pairs) :
"${pair.role}-${pair.member}" => pair
}
parent_type = var.parent == null ? null : split("/", var.parent)[0]
parent_id = var.parent == null ? null : split("/", var.parent)[1]
prefix = var.prefix == null ? "" : "${var.prefix}-"
@ -69,31 +34,6 @@ locals {
name = try(data.google_project.project.0.name, null)
}
)
logging_sinks = coalesce(var.logging_sinks, {})
sink_type_destination = {
gcs = "storage.googleapis.com"
bigquery = "bigquery.googleapis.com"
pubsub = "pubsub.googleapis.com"
logging = "logging.googleapis.com"
}
sink_bindings = {
for type in ["gcs", "bigquery", "pubsub", "logging"] :
type => {
for name, sink in local.logging_sinks :
name => sink
if sink.iam && sink.type == type
}
}
service_encryption_key_ids = distinct(flatten([
for s in keys(var.service_encryption_key_ids) : [
for ss in try(local._service_encryption_key_dependent_services[s], [s]) : [
for key in var.service_encryption_key_ids[s] : {
service = ss
key = key
} if key != null
]
]
]))
}
data "google_project" "project" {
@ -113,13 +53,12 @@ resource "google_project" "project" {
skip_delete = var.skip_delete
}
resource "google_project_iam_custom_role" "roles" {
for_each = var.custom_roles
project = local.project.project_id
role_id = each.key
title = "Custom role ${each.key}"
description = "Terraform-managed"
permissions = each.value
resource "google_project_service" "project_services" {
for_each = toset(var.services)
project = local.project.project_id
service = each.value
disable_on_destroy = var.service_config.disable_on_destroy
disable_dependent_services = var.service_config.disable_dependent_services
}
resource "google_compute_project_metadata_item" "oslogin_meta" {
@ -139,232 +78,6 @@ resource "google_resource_manager_lien" "lien" {
reason = var.lien_reason
}
resource "google_project_service" "project_services" {
for_each = toset(var.services)
project = local.project.project_id
service = each.value
disable_on_destroy = var.service_config.disable_on_destroy
disable_dependent_services = var.service_config.disable_dependent_services
}
# IAM notes:
# - external users need to have accepted the invitation email to join
# - oslogin roles also require role to list instances
# - additive (non-authoritative) roles might fail due to dynamic values
resource "google_project_iam_binding" "authoritative" {
for_each = local.iam
project = local.project.project_id
role = each.key
members = each.value
depends_on = [
google_project_service.project_services,
google_project_iam_custom_role.roles
]
}
resource "google_project_iam_member" "additive" {
for_each = (
length(var.iam_additive) + length(var.iam_additive_members) > 0
? local.iam_additive
: {}
)
project = local.project.project_id
role = each.value.role
member = each.value.member
depends_on = [
google_project_service.project_services,
google_project_iam_custom_role.roles
]
}
resource "google_project_iam_member" "oslogin_iam_serviceaccountuser" {
for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([])
project = local.project.project_id
role = "roles/iam.serviceAccountUser"
member = each.value
}
resource "google_project_iam_member" "oslogin_compute_viewer" {
for_each = var.oslogin ? toset(distinct(concat(var.oslogin_admins, var.oslogin_users))) : toset([])
project = local.project.project_id
role = "roles/compute.viewer"
member = each.value
}
resource "google_project_iam_member" "oslogin_admins" {
for_each = var.oslogin ? toset(var.oslogin_admins) : toset([])
project = local.project.project_id
role = "roles/compute.osAdminLogin"
member = each.value
}
resource "google_project_iam_member" "oslogin_users" {
for_each = var.oslogin ? toset(var.oslogin_users) : toset([])
project = local.project.project_id
role = "roles/compute.osLogin"
member = each.value
}
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
}
}
}
resource "google_project_organization_policy" "list" {
for_each = var.policy_list
project = local.project.project_id
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_compute_shared_vpc_host_project" "shared_vpc_host" {
count = try(var.shared_vpc_host_config.enabled, false) ? 1 : 0
project = local.project.project_id
}
resource "google_compute_shared_vpc_service_project" "service_projects" {
for_each = (
try(var.shared_vpc_host_config.enabled, false)
? toset(coalesce(var.shared_vpc_host_config.service_projects, []))
: toset([])
)
host_project = local.project.project_id
service_project = each.value
depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host]
}
resource "google_compute_shared_vpc_service_project" "shared_vpc_service" {
count = try(var.shared_vpc_service_config.attach, false) ? 1 : 0
host_project = var.shared_vpc_service_config.host_project
service_project = local.project.project_id
}
resource "google_logging_project_sink" "sink" {
for_each = local.logging_sinks
name = each.key
#description = "${each.key} (Terraform-managed)"
project = local.project.project_id
destination = "${local.sink_type_destination[each.value.type]}/${each.value.destination}"
filter = each.value.filter
unique_writer_identity = each.value.unique_writer
dynamic "exclusions" {
for_each = each.value.exclusions
iterator = exclusion
content {
name = exclusion.key
filter = exclusion.value
}
}
depends_on = [
google_project_iam_binding.authoritative,
google_project_iam_member.additive
]
}
resource "google_storage_bucket_iam_member" "gcs-sinks-binding" {
for_each = local.sink_bindings["gcs"]
bucket = each.value.destination
role = "roles/storage.objectCreator"
member = google_logging_project_sink.sink[each.key].writer_identity
}
resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" {
for_each = local.sink_bindings["bigquery"]
project = split("/", each.value.destination)[1]
dataset_id = split("/", each.value.destination)[3]
role = "roles/bigquery.dataEditor"
member = google_logging_project_sink.sink[each.key].writer_identity
}
resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" {
for_each = local.sink_bindings["pubsub"]
project = split("/", each.value.destination)[1]
topic = split("/", each.value.destination)[3]
role = "roles/pubsub.publisher"
member = google_logging_project_sink.sink[each.key].writer_identity
}
resource "google_project_iam_member" "bucket-sinks-binding" {
for_each = local.sink_bindings["logging"]
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_project_sink.sink[each.key].writer_identity
# TODO(jccb): use a condition to limit writer-identity only to this
# bucket
}
resource "google_logging_project_exclusion" "logging-exclusion" {
for_each = coalesce(var.logging_exclusions, {})
name = each.key
project = local.project.project_id
description = "${each.key} (Terraform-managed)"
filter = each.value
}
resource "google_essential_contacts_contact" "contact" {
provider = google-beta
for_each = var.contacts
@ -374,43 +87,6 @@ resource "google_essential_contacts_contact" "contact" {
notification_category_subscriptions = each.value
}
resource "google_access_context_manager_service_perimeter_resource" "service-perimeter-resource-standard" {
count = var.service_perimeter_standard != null ? 1 : 0
# If used, remember to uncomment 'lifecycle' block in the
# modules/vpc-sc/google_access_context_manager_service_perimeter resource.
perimeter_name = var.service_perimeter_standard
resource = "projects/${local.project.number}"
}
resource "google_access_context_manager_service_perimeter_resource" "service-perimeter-resource-bridges" {
for_each = toset(var.service_perimeter_bridges != null ? var.service_perimeter_bridges : [])
# If used, remember to uncomment 'lifecycle' block in the
# modules/vpc-sc/google_access_context_manager_service_perimeter resource.
perimeter_name = each.value
resource = "projects/${local.project.number}"
}
resource "google_kms_crypto_key_iam_member" "crypto_key" {
for_each = {
for service_key in local.service_encryption_key_ids :
"${service_key.service}.${service_key.key}" => service_key
if service_key != service_key.key
}
crypto_key_id = each.value.key
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
member = "serviceAccount:${local.service_accounts_robots[each.value.service]}"
depends_on = [
google_project.project,
google_project_service.project_services,
google_project_service_identity.jit_si,
data.google_bigquery_default_service_account.bq_sa,
data.google_project.project,
data.google_storage_project_service_account.gcs_sa,
]
}
resource "google_monitoring_monitored_project" "primary" {
provider = google-beta
for_each = toset(coalesce(var.metric_scopes, []))

View File

@ -0,0 +1,90 @@
/**
* 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 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
}
}
}
resource "google_project_organization_policy" "list" {
for_each = var.policy_list
project = local.project.project_id
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
}
}
}

View File

@ -30,7 +30,7 @@ output "name" {
google_project_organization_policy.list,
google_project_service.project_services,
google_compute_shared_vpc_service_project.service_projects,
google_kms_crypto_key_iam_member.crypto_key
google_kms_crypto_key_iam_member.service_identity_cmek
]
}
@ -42,7 +42,7 @@ output "number" {
google_project_organization_policy.list,
google_project_service.project_services,
google_compute_shared_vpc_service_project.service_projects,
google_kms_crypto_key_iam_member.crypto_key
google_kms_crypto_key_iam_member.service_identity_cmek
]
}
@ -56,7 +56,7 @@ output "project_id" {
google_project_organization_policy.list,
google_project_service.project_services,
google_compute_shared_vpc_service_project.service_projects,
google_kms_crypto_key_iam_member.crypto_key
google_kms_crypto_key_iam_member.service_identity_cmek
]
}
@ -69,7 +69,7 @@ output "service_accounts" {
}
depends_on = [
google_project_service.project_services,
google_kms_crypto_key_iam_member.crypto_key,
google_kms_crypto_key_iam_member.service_identity_cmek,
google_project_service_identity.jit_si,
data.google_bigquery_default_service_account.bq_sa,
data.google_storage_project_service_account.gcs_sa

View File

@ -14,38 +14,59 @@
* limitations under the License.
*/
# tfdoc:file:description Service identities and supporting resources.
locals {
service_account_cloud_services = "${local.project.number}@cloudservices.gserviceaccount.com"
_service_accounts_cmek_service_dependencies = {
"composer" : [
"composer",
"artifactregistry", "container-engine", "compute", "pubsub", "storage"
]
"dataflow" : ["dataflow", "compute"]
}
_service_accounts_robot_services = {
artifactregistry = "service-%s@gcp-sa-artifactregistry"
bq = "bq-%s@bigquery-encryption"
cloudasset = "service-%s@gcp-sa-cloudasset"
cloudbuild = "service-%s@gcp-sa-cloudbuild"
composer = "service-%s@cloudcomposer-accounts"
compute = "service-%s@compute-system"
container-engine = "service-%s@container-engine-robot"
containerregistry = "service-%s@containerregistry"
dataflow = "service-%s@dataflow-service-producer-prod"
dataproc = "service-%s@dataproc-accounts"
gae-flex = "service-%s@gae-api-prod"
gcf = "service-%s@gcf-admin-robot"
pubsub = "service-%s@gcp-sa-pubsub"
secretmanager = "service-%s@gcp-sa-secretmanager"
storage = "service-%s@gs-project-accounts"
}
service_accounts_default = {
compute = "${local.project.number}-compute@developer.gserviceaccount.com"
gae = "${local.project.project_id}@appspot.gserviceaccount.com"
}
service_accounts_robot_services = {
artifactregistry = "gcp-sa-artifactregistry"
bq = "bigquery-encryption"
cloudasset = "gcp-sa-cloudasset"
cloudbuild = "gcp-sa-cloudbuild"
composer = "cloudcomposer-accounts"
compute = "compute-system"
container-engine = "container-engine-robot"
containerregistry = "containerregistry"
dataflow = "dataflow-service-producer-prod"
dataproc = "dataproc-accounts"
gae-flex = "gae-api-prod"
gcf = "gcf-admin-robot"
pubsub = "gcp-sa-pubsub"
secretmanager = "gcp-sa-secretmanager"
storage = "gs-project-accounts"
}
service_account_cloud_services = (
"${local.project.number}@cloudservices.gserviceaccount.com"
)
service_accounts_robots = {
for service, name in local.service_accounts_robot_services :
service => "${service == "bq" ? "bq" : "service"}-${local.project.number}@${name}.iam.gserviceaccount.com"
for k, v in local._service_accounts_robot_services :
k => "${format(v, local.project.number)}.iam.gserviceaccount.com"
}
jit_services = [
service_accounts_jit_services = [
"secretmanager.googleapis.com",
"pubsub.googleapis.com",
"cloudasset.googleapis.com"
]
service_accounts_cmek_service_keys = distinct(flatten([
for s in keys(var.service_encryption_key_ids) : [
for ss in try(local._service_accounts_cmek_service_dependencies[s], [s]) : [
for key in var.service_encryption_key_ids[s] : {
service = ss
key = key
} if key != null
]
]
]))
}
data "google_storage_project_service_account" "gcs_sa" {
@ -62,9 +83,28 @@ data "google_bigquery_default_service_account" "bq_sa" {
# Secret Manager SA created just in time, we need to trigger the creation.
resource "google_project_service_identity" "jit_si" {
for_each = setintersection(var.services, local.jit_services)
for_each = setintersection(var.services, local.service_accounts_jit_services)
provider = google-beta
project = local.project.project_id
service = each.value
depends_on = [google_project_service.project_services]
}
resource "google_kms_crypto_key_iam_member" "service_identity_cmek" {
for_each = {
for service_key in local.service_accounts_cmek_service_keys :
"${service_key.service}.${service_key.key}" => service_key
if service_key != service_key.key
}
crypto_key_id = each.value.key
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
member = "serviceAccount:${local.service_accounts_robots[each.value.service]}"
depends_on = [
google_project.project,
google_project_service.project_services,
google_project_service_identity.jit_si,
data.google_bigquery_default_service_account.bq_sa,
data.google_project.project,
data.google_storage_project_service_account.gcs_sa,
]
}

View File

@ -0,0 +1,39 @@
/**
* 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 Shared VPC project-level configuration.
resource "google_compute_shared_vpc_host_project" "shared_vpc_host" {
count = try(var.shared_vpc_host_config.enabled, false) ? 1 : 0
project = local.project.project_id
}
resource "google_compute_shared_vpc_service_project" "service_projects" {
for_each = (
try(var.shared_vpc_host_config.enabled, false)
? toset(coalesce(var.shared_vpc_host_config.service_projects, []))
: toset([])
)
host_project = local.project.project_id
service_project = each.value
depends_on = [google_compute_shared_vpc_host_project.shared_vpc_host]
}
resource "google_compute_shared_vpc_service_project" "shared_vpc_service" {
count = try(var.shared_vpc_service_config.attach, false) ? 1 : 0
host_project = var.shared_vpc_service_config.host_project
service_project = local.project.project_id
}

45
modules/project/vpc-sc.tf Normal file
View File

@ -0,0 +1,45 @@
/**
* 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 VPC-SC project-level perimeter configuration.
moved {
from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-standard
to = google_access_context_manager_service_perimeter_resource.standard
}
resource "google_access_context_manager_service_perimeter_resource" "standard" {
count = var.service_perimeter_standard != null ? 1 : 0
# this needs an additional lifecycle block in the vpc module on the
# google_access_context_manager_service_perimeter resource
perimeter_name = var.service_perimeter_standard
resource = "projects/${local.project.number}"
}
moved {
from = google_access_context_manager_service_perimeter_resource.service-perimeter-resource-bridges
to = google_access_context_manager_service_perimeter_resource.bridge
}
resource "google_access_context_manager_service_perimeter_resource" "bridge" {
for_each = toset(
var.service_perimeter_bridges != null ? var.service_perimeter_bridges : []
)
# this needs an additional lifecycle block in the vpc module on the
# google_access_context_manager_service_perimeter resource
perimeter_name = each.value
resource = "projects/${local.project.number}"
}

View File

@ -39,7 +39,7 @@ State = enum.Enum('State', 'OK FAIL SKIP')
def _check_dir(dir_name, files=False, show_extra=False):
'Invoke tfdoc on folder, using the relevant options.'
dir_path = BASEDIR / dir_name
for readme_path in dir_path.glob('**/README.md'):
for readme_path in sorted(dir_path.glob('**/README.md')):
if '.terraform' in str(readme_path):
continue
diff = None
@ -50,8 +50,9 @@ def _check_dir(dir_name, files=False, show_extra=False):
state = State.SKIP
else:
try:
new_doc = tfdoc.create_doc(
readme_path.parent, files=files, show_extra=show_extra)
new_doc = tfdoc.create_doc(readme_path.parent, files=files,
show_extra=show_extra, exclude_files=None,
readme=readme)
except SystemExit:
state = state.SKIP
else:
@ -74,6 +75,7 @@ def _check_dir(dir_name, files=False, show_extra=False):
@ click.option('--show-extra/--no-show-extra', default=False)
def main(dirs, files=False, show_diffs=False, show_extra=False):
'Cycle through modules and ensure READMEs are up-to-date.'
print(f'files: {files}, extra: {show_extra}, diffs: {show_diffs}\n')
errors = []
state_labels = {State.FAIL: '', State.OK: '', State.SKIP: '?'}
for dir_name in dirs:

View File

@ -71,6 +71,7 @@ FILE_RE_RESOURCES = re.compile(
HEREDOC_RE = re.compile(r'(?sm)^<<\-?END(\s*.*?)\s*END$')
MARK_BEGIN = '<!-- BEGIN TFDOC -->'
MARK_END = '<!-- END TFDOC -->'
MARK_OPTS_RE = re.compile(r'(?sm)<!-- TFDOC OPTS ((?:[a-z_]+:[0-1]\s*?)+) -->')
OUT_ENUM = enum.Enum('O', 'OPEN ATTR ATTR_DATA CLOSE COMMENT TXT SKIP')
OUT_RE = re.compile(r'''(?smx)
# output open
@ -237,8 +238,16 @@ def format_doc(outputs, variables, files, show_extra=False):
def format_files(items):
'Format files table.'
items.sort(key=lambda i: i.name)
yield '| name | description | modules | resources |'
yield '|---|---|---|---|'
num_modules = sum(len(i.modules) for i in items)
num_resources = sum(len(i.resources) for i in items)
yield '| name | description |{}{}'.format(
' modules |' if num_modules else '',
' resources |' if num_resources else ''
)
yield '|---|---|{}{}'.format(
'---|' if num_modules else '',
'---|' if num_resources else ''
)
for i in items:
modules = resources = ''
if i.modules:
@ -247,7 +256,11 @@ def format_files(items):
if i.resources:
resources = '<code>%s</code>' % '</code> · <code>'.join(
sorted(i.resources))
yield f'| [{i.name}](./{i.name}) | {i.description} | {modules} | {resources} |'
yield '| [{}](./{}) | {} |{}{}'.format(
i.name, i.name, i.description,
f' {modules} |' if num_modules else '',
f' {resources} |' if num_resources else ''
)
def format_outputs(items, show_extra=True):
@ -321,7 +334,28 @@ def get_doc(readme):
return {'doc': m.group(1), 'start': m.start(), 'end': m.end()}
def create_doc(module_path, files=False, show_extra=False, exclude_files=None):
def get_doc_opts(readme):
'Check if README file is setting options via a mark, and return options.'
m = MARK_OPTS_RE.search(readme)
opts = {}
if not m:
return opts
try:
for o in m.group(1).split():
k, v = o.split(':')
opts[k] = bool(int(v))
except (TypeError, ValueError) as e:
raise SystemExit(f'incorrect option mark: {e}')
return opts
def create_doc(module_path, files=False, show_extra=False, exclude_files=None,
readme=None):
if readme:
# check for overrides in doc
opts = get_doc_opts(readme)
files = opts.get('files', files)
show_extra = opts.get('show_extra', show_extra)
try:
mod_files = list(parse_files(module_path, exclude_files)) if files else []
mod_variables = list(parse_variables(module_path))
@ -331,13 +365,17 @@ def create_doc(module_path, files=False, show_extra=False, exclude_files=None):
return format_doc(mod_outputs, mod_variables, mod_files, show_extra)
def replace_doc(module_path, doc):
'Replace document in module\'s README.md file.'
readme_path = os.path.join(module_path, 'README.md')
def get_readme(readme_path):
'Open and return README.md in module.'
try:
readme = open(readme_path).read()
return open(readme_path).read()
except (IOError, OSError) as e:
raise SystemExit(f'Error opening README {readme_path}: {e}')
def replace_doc(readme_path, doc, readme=None):
'Replace document in module\'s README.md file.'
readme = readme or get_readme(readme_path)
result = get_doc(readme)
if not result:
raise SystemExit(f'Mark not found in README {readme_path}')
@ -356,17 +394,19 @@ def replace_doc(module_path, doc):
@ click.command()
@ click.argument('module', type=click.Path(exists=True))
@ click.argument('module_path', type=click.Path(exists=True))
@click.option('--exclude-file', '-x', multiple=True)
@ click.option('--files/--no-files', default=False)
@ click.option('--replace/--no-replace', default=True)
@ click.option('--show-extra/--no-show-extra', default=False)
def main(module=None, annotate=False, exclude_file=None, files=False, replace=True,
def main(module_path=None, exclude_file=None, files=False, replace=True,
show_extra=True):
'Program entry point.'
doc = create_doc(module, files, show_extra, exclude_file)
readme_path = os.path.join(module_path, 'README.md')
readme = get_readme(readme_path)
doc = create_doc(module_path, files, show_extra, exclude_file, readme)
if replace:
replace_doc(module, doc)
replace_doc(readme_path, doc, readme)
else:
print(doc)