CloudSQL PSC Endpoints support (#2242)

* Add PSC endpoints consumers to net-address
* Cloud SQL E2E tests
This commit is contained in:
Wiktor Niesiobędzki
2024-05-12 12:00:39 +02:00
committed by GitHub
parent 35a17a46ba
commit 6a3c7fe444
19 changed files with 464 additions and 178 deletions

View File

@@ -1,4 +1,4 @@
# Cloud SQL instance with read replicas # Cloud SQL instance module
This module manages the creation of Cloud SQL instances with potential read replicas in other regions. It can also create an initial set of users and databases via the `users` and `databases` parameters. This module manages the creation of Cloud SQL instances with potential read replicas in other regions. It can also create an initial set of users and databases via the `users` and `databases` parameters.
@@ -6,7 +6,24 @@ Note that this module assumes that some options are the same for both the primar
*Warning:* if you use the `users` field, you terraform state will contain each user's password in plain text. *Warning:* if you use the `users` field, you terraform state will contain each user's password in plain text.
## Simple example <!-- BEGIN TOC -->
- [Examples](#examples)
- [Simple example](#simple-example)
- [Cross-regional read replica](#cross-regional-read-replica)
- [Custom flags, databases and users](#custom-flags-databases-and-users)
- [CMEK encryption](#cmek-encryption)
- [Instance with PSC enabled](#instance-with-psc-enabled)
- [Enable public IP](#enable-public-ip)
- [Query Insights](#query-insights)
- [Maintenance Config](#maintenance-config)
- [SSL Config](#ssl-config)
- [Variables](#variables)
- [Outputs](#outputs)
- [Fixtures](#fixtures)
<!-- END TOC -->
## Examples
### Simple example
This example shows how to setup a project, VPC and a standalone Cloud SQL instance. This example shows how to setup a project, VPC and a standalone Cloud SQL instance.
@@ -14,10 +31,12 @@ This example shows how to setup a project, VPC and a standalone Cloud SQL instan
module "project" { module "project" {
source = "./fabric/modules/project" source = "./fabric/modules/project"
billing_account = var.billing_account_id billing_account = var.billing_account_id
parent = var.organization_id parent = var.folder_id
name = "my-db-project" name = "db-prj"
prefix = var.prefix
services = [ services = [
"servicenetworking.googleapis.com" "servicenetworking.googleapis.com",
"sqladmin.googleapis.com",
] ]
} }
@@ -25,9 +44,18 @@ module "vpc" {
source = "./fabric/modules/net-vpc" source = "./fabric/modules/net-vpc"
project_id = module.project.project_id project_id = module.project.project_id
name = "my-network" name = "my-network"
# need only one - psa_config or subnets_psc
psa_configs = [{ psa_configs = [{
ranges = { cloud-sql = "10.60.0.0/16" } ranges = { cloud-sql = "10.60.0.0/16" }
deletion_policy = "ABANDON"
}] }]
subnets_psc = [
{
ip_cidr_range = "10.0.3.0/24"
name = "psc"
region = var.region
}
]
} }
module "db" { module "db" {
@@ -38,17 +66,20 @@ module "db" {
psa_config = { psa_config = {
private_network = module.vpc.self_link private_network = module.vpc.self_link
} }
# psc_allowed_consumer_projects = [var.project_id]
} }
} }
name = "db" name = "db"
region = "europe-west1" region = var.region
database_version = "POSTGRES_13" database_version = "POSTGRES_13"
tier = "db-g1-small" tier = "db-g1-small"
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=3 resources=11 inventory=simple.yaml # tftest modules=3 resources=14 inventory=simple.yaml e2e
``` ```
## Cross-regional read replica ### Cross-regional read replica
```hcl ```hcl
module "db" { module "db" {
@@ -61,21 +92,23 @@ module "db" {
} }
} }
} }
prefix = "myprefix"
name = "db" name = "db"
region = "europe-west1" prefix = "myprefix"
region = var.region
database_version = "POSTGRES_13" database_version = "POSTGRES_13"
tier = "db-g1-small" tier = "db-g1-small"
replicas = { replicas = {
replica1 = { region = "europe-west3", encryption_key_name = null } replica1 = { region = "europe-west3" }
replica2 = { region = "us-central1", encryption_key_name = null } replica2 = { region = "us-central1" }
} }
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=1 resources=3 inventory=replicas.yaml # tftest modules=1 resources=3 inventory=replicas.yaml e2e
``` ```
## Custom flags, databases and users ### Custom flags, databases and users
```hcl ```hcl
module "db" { module "db" {
@@ -89,7 +122,7 @@ module "db" {
} }
} }
name = "db" name = "db"
region = "europe-west1" region = var.region
database_version = "MYSQL_8_0" database_version = "MYSQL_8_0"
tier = "db-g1-small" tier = "db-g1-small"
@@ -112,47 +145,19 @@ module "db" {
password = "mypassword" password = "mypassword"
} }
} }
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=1 resources=6 inventory=custom.yaml # tftest modules=1 resources=6 inventory=custom.yaml e2e
``` ```
### CMEK encryption ### CMEK encryption
```hcl ```hcl
module "project" {
source = "./fabric/modules/project"
billing_account = var.billing_account_id
parent = var.organization_id
name = "my-db-project"
services = [
"servicenetworking.googleapis.com",
"sqladmin.googleapis.com",
]
}
module "kms" {
source = "./fabric/modules/kms"
project_id = module.project.project_id
keyring = {
name = "keyring"
location = var.region
}
keys = {
key-sql = {
iam = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
"serviceAccount:${module.project.service_accounts.robots.sqladmin}"
]
}
}
}
}
module "db" { module "db" {
source = "./fabric/modules/cloudsql-instance" source = "./fabric/modules/cloudsql-instance"
project_id = module.project.project_id project_id = var.project_id
encryption_key_name = module.kms.keys["key-sql"].id encryption_key_name = var.kms_key.id
network_config = { network_config = {
connectivity = { connectivity = {
psa_config = { psa_config = {
@@ -160,13 +165,15 @@ module "db" {
} }
} }
} }
name = "db" name = "db"
region = var.region region = var.region
database_version = "POSTGRES_13" database_version = "POSTGRES_13"
tier = "db-g1-small" tier = "db-g1-small"
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=3 resources=10 # tftest modules=1 resources=2 fixtures=fixtures/cloudsql-kms-iam-grant.tf e2e
``` ```
### Instance with PSC enabled ### Instance with PSC enabled
@@ -177,22 +184,25 @@ module "db" {
project_id = var.project_id project_id = var.project_id
network_config = { network_config = {
connectivity = { connectivity = {
psc_allowed_consumer_projects = ["my-project-id"] psc_allowed_consumer_projects = [var.project_id]
} }
} }
prefix = "myprefix" prefix = "myprefix"
name = "db" name = "db"
region = "europe-west1" region = var.region
availability_type = "REGIONAL" availability_type = "REGIONAL"
database_version = "POSTGRES_13" database_version = "POSTGRES_13"
tier = "db-g1-small" tier = "db-g1-small"
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=1 resources=1 # tftest modules=1 resources=1 inventory=psc.yaml e2e
``` ```
### Enable public IP ### Enable public IP
Use `ipv_enabled` to create instances with a public IP. Use `public_ipv4` to create instances with a public IP.
```hcl ```hcl
module "db" { module "db" {
@@ -206,15 +216,14 @@ module "db" {
} }
} }
} }
name = "db" name = "db"
region = "europe-west1" region = var.region
tier = "db-g1-small" tier = "db-g1-small"
database_version = "MYSQL_8_0" database_version = "MYSQL_8_0"
replicas = { gcp_deletion_protection = false
replica1 = { region = "europe-west3", encryption_key_name = null } terraform_deletion_protection = false
}
} }
# tftest modules=1 resources=2 inventory=public-ip.yaml # tftest modules=1 resources=1 inventory=public-ip.yaml e2e
``` ```
### Query Insights ### Query Insights
@@ -233,15 +242,17 @@ module "db" {
} }
} }
name = "db" name = "db"
region = "europe-west1" region = var.region
database_version = "POSTGRES_13" database_version = "POSTGRES_13"
tier = "db-g1-small" tier = "db-g1-small"
insights_config = { insights_config = {
query_string_length = 2048 query_string_length = 2048
} }
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=1 resources=1 inventory=insights.yaml # tftest modules=1 resources=1 inventory=insights.yaml e2e
``` ```
### Maintenance Config ### Maintenance Config
@@ -260,13 +271,15 @@ module "db" {
} }
} }
name = "db" name = "db"
region = "europe-west1" region = var.region
database_version = "POSTGRES_13" database_version = "POSTGRES_13"
tier = "db-g1-small" tier = "db-g1-small"
maintenance_config = {} maintenance_config = {}
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=1 resources=1 # tftest modules=1 resources=1 e2e
``` ```
### SSL Config ### SSL Config
@@ -285,13 +298,15 @@ module "db" {
} }
} }
name = "db" name = "db"
region = "europe-west1" region = var.region
database_version = "POSTGRES_13" database_version = "POSTGRES_13"
tier = "db-g1-small" tier = "db-g1-small"
ssl = {} ssl = {}
gcp_deletion_protection = false
terraform_deletion_protection = false
} }
# tftest modules=1 resources=1 # tftest modules=1 resources=1 e2e
``` ```
<!-- BEGIN TFDOC --> <!-- BEGIN TFDOC -->
## Variables ## Variables
@@ -322,7 +337,7 @@ module "db" {
| [labels](variables.tf#L140) | Labels to be attached to all instances. | <code>map&#40;string&#41;</code> | | <code>null</code> | | [labels](variables.tf#L140) | Labels to be attached to all instances. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [maintenance_config](variables.tf#L146) | Set maintenance window configuration and maintenance deny period (up to 90 days). Date format: 'yyyy-mm-dd'. | <code title="object&#40;&#123;&#10; maintenance_window &#61; optional&#40;object&#40;&#123;&#10; day &#61; number&#10; hour &#61; number&#10; update_track &#61; optional&#40;string, null&#41;&#10; &#125;&#41;, null&#41;&#10; deny_maintenance_period &#61; optional&#40;object&#40;&#123;&#10; start_date &#61; string&#10; end_date &#61; string&#10; start_time &#61; optional&#40;string, &#34;00:00:00&#34;&#41;&#10; &#125;&#41;, null&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | | [maintenance_config](variables.tf#L146) | Set maintenance window configuration and maintenance deny period (up to 90 days). Date format: 'yyyy-mm-dd'. | <code title="object&#40;&#123;&#10; maintenance_window &#61; optional&#40;object&#40;&#123;&#10; day &#61; number&#10; hour &#61; number&#10; update_track &#61; optional&#40;string, null&#41;&#10; &#125;&#41;, null&#41;&#10; deny_maintenance_period &#61; optional&#40;object&#40;&#123;&#10; start_date &#61; string&#10; end_date &#61; string&#10; start_time &#61; optional&#40;string, &#34;00:00:00&#34;&#41;&#10; &#125;&#41;, null&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [prefix](variables.tf#L207) | Optional prefix used to generate instance names. | <code>string</code> | | <code>null</code> | | [prefix](variables.tf#L207) | Optional prefix used to generate instance names. | <code>string</code> | | <code>null</code> |
| [replicas](variables.tf#L227) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; encryption_key_name &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | | [replicas](variables.tf#L227) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; encryption_key_name &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [root_password](variables.tf#L236) | Root password of the Cloud SQL instance. Required for MS SQL Server. | <code>string</code> | | <code>null</code> | | [root_password](variables.tf#L236) | Root password of the Cloud SQL instance. Required for MS SQL Server. | <code>string</code> | | <code>null</code> |
| [ssl](variables.tf#L242) | Setting to enable SSL, set config and certificates. | <code title="object&#40;&#123;&#10; client_certificates &#61; optional&#40;list&#40;string&#41;&#41;&#10; require_ssl &#61; optional&#40;bool&#41;&#10; ssl_mode &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> | | [ssl](variables.tf#L242) | Setting to enable SSL, set config and certificates. | <code title="object&#40;&#123;&#10; client_certificates &#61; optional&#40;list&#40;string&#41;&#41;&#10; require_ssl &#61; optional&#40;bool&#41;&#10; ssl_mode &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [terraform_deletion_protection](variables.tf#L258) | Prevent terraform from deleting instances. | <code>bool</code> | | <code>true</code> | | [terraform_deletion_protection](variables.tf#L258) | Prevent terraform from deleting instances. | <code>bool</code> | | <code>true</code> |
@@ -350,4 +365,8 @@ module "db" {
| [self_link](outputs.tf#L114) | Self link of the primary instance. | | | [self_link](outputs.tf#L114) | Self link of the primary instance. | |
| [self_links](outputs.tf#L119) | Self links of all instances. | | | [self_links](outputs.tf#L119) | Self links of all instances. | |
| [user_passwords](outputs.tf#L127) | Map of containing the password of all users created through terraform. | ✓ | | [user_passwords](outputs.tf#L127) | Map of containing the password of all users created through terraform. | ✓ |
## Fixtures
- [cloudsql-kms-iam-grant.tf](../../tests/fixtures/cloudsql-kms-iam-grant.tf)
<!-- END TFDOC --> <!-- END TFDOC -->

View File

@@ -228,7 +228,7 @@ variable "replicas" {
description = "Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation." description = "Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation."
type = map(object({ type = map(object({
region = string region = string
encryption_key_name = string encryption_key_name = optional(string)
})) }))
default = {} default = {}
} }

View File

@@ -122,6 +122,26 @@ module "addresses" {
# tftest modules=1 resources=1 inventory=psc.yaml e2e # tftest modules=1 resources=1 inventory=psc.yaml e2e
``` ```
To create PSC address targeting a service regional provider use the `service_attachment` property.
```hcl
module "addresses" {
source = "./fabric/modules/net-address"
project_id = var.project_id
psc_addresses = {
cloudsql-one = {
address = "10.0.16.32"
subnet_self_link = var.subnet.self_link
region = var.region
service_attachment = {
psc_service_attachment_link = module.cloudsql-instance.psc_service_attachment_link
}
}
}
}
# tftest modules=2 resources=3 fixtures=fixtures/cloudsql-instance.tf inventory=psc-service-attachment.yaml e2e
```
### IPSec Interconnect addresses ### IPSec Interconnect addresses
```hcl ```hcl
@@ -176,8 +196,8 @@ module "addresses" {
| [internal_addresses](variables.tf#L50) | Map of internal addresses to create, keyed by name. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; subnetwork &#61; string&#10; address &#61; optional&#40;string&#41;&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; ipv6 &#61; optional&#40;map&#40;string&#41;&#41; &#35; To be left empty for ipv6&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; name &#61; optional&#40;string&#41;&#10; purpose &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | | [internal_addresses](variables.tf#L50) | Map of internal addresses to create, keyed by name. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; subnetwork &#61; string&#10; address &#61; optional&#40;string&#41;&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; ipv6 &#61; optional&#40;map&#40;string&#41;&#41; &#35; To be left empty for ipv6&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; name &#61; optional&#40;string&#41;&#10; purpose &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [ipsec_interconnect_addresses](variables.tf#L65) | Map of internal addresses used for HPA VPN over Cloud Interconnect. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; address &#61; string&#10; network &#61; string&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; name &#61; optional&#40;string&#41;&#10; prefix_length &#61; number&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | | [ipsec_interconnect_addresses](variables.tf#L65) | Map of internal addresses used for HPA VPN over Cloud Interconnect. | <code title="map&#40;object&#40;&#123;&#10; region &#61; string&#10; address &#61; string&#10; network &#61; string&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; name &#61; optional&#40;string&#41;&#10; prefix_length &#61; number&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [network_attachments](variables.tf#L84) | PSC network attachments, names as keys. | <code title="map&#40;object&#40;&#123;&#10; subnet_self_link &#61; string&#10; automatic_connection &#61; optional&#40;bool, false&#41;&#10; description &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; producer_accept_lists &#61; optional&#40;list&#40;string&#41;&#41;&#10; producer_reject_lists &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | | [network_attachments](variables.tf#L84) | PSC network attachments, names as keys. | <code title="map&#40;object&#40;&#123;&#10; subnet_self_link &#61; string&#10; automatic_connection &#61; optional&#40;bool, false&#41;&#10; description &#61; optional&#40;string, &#34;Terraform-managed.&#34;&#41;&#10; producer_accept_lists &#61; optional&#40;list&#40;string&#41;&#41;&#10; producer_reject_lists &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [psa_addresses](variables.tf#L102) | Map of internal addresses used for Private Service Access. | <code title="map&#40;object&#40;&#123;&#10; address &#61; string&#10; network &#61; string&#10; prefix_length &#61; number&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; name &#61; optional&#40;string&#41;&#10;&#10;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | | [psa_addresses](variables.tf#L102) | Map of internal addresses used for Private Service Access. | <code title="map&#40;object&#40;&#123;&#10; address &#61; string&#10; network &#61; string&#10; prefix_length &#61; number&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; name &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [psc_addresses](variables.tf#L115) | Map of internal addresses used for Private Service Connect. | <code title="map&#40;object&#40;&#123;&#10; address &#61; string&#10; network &#61; string&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; name &#61; optional&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | | [psc_addresses](variables.tf#L114) | Map of internal addresses used for Private Service Connect. | <code title="map&#40;object&#40;&#123;&#10; address &#61; string&#10; description &#61; optional&#40;string, &#34;Terraform managed.&#34;&#41;&#10; name &#61; optional&#40;string&#41;&#10; network &#61; optional&#40;string&#41;&#10; region &#61; optional&#40;string&#41;&#10; subnet_self_link &#61; optional&#40;string&#41;&#10; service_attachment &#61; optional&#40;object&#40;&#123; &#35; so we can safely check if service_attachemnt &#33;&#61; null in for_each&#10; psc_service_attachment_link &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs ## Outputs
@@ -193,5 +213,6 @@ module "addresses" {
## Fixtures ## Fixtures
- [cloudsql-instance.tf](../../tests/fixtures/cloudsql-instance.tf)
- [net-vpc-ipv6.tf](../../tests/fixtures/net-vpc-ipv6.tf) - [net-vpc-ipv6.tf](../../tests/fixtures/net-vpc-ipv6.tf)
<!-- END TFDOC --> <!-- END TFDOC -->

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright 2022 Google LLC * Copyright 2024 Google LLC
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,20 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
locals {
network_attachments = {
for k, v in var.network_attachments : k => merge(v, {
region = regex("regions/([^/]+)", v.subnet_self_link)[0]
# not using the full self link generates a permadiff
subnet_self_link = (
startswith(v.subnet_self_link, "https://")
? v.subnet_self_link
: "https://www.googleapis.com/compute/v1/${v.subnet_self_link}"
)
})
}
}
resource "google_compute_global_address" "global" { resource "google_compute_global_address" "global" {
for_each = var.global_addresses for_each = var.global_addresses
project = var.project_id project = var.project_id
@@ -66,18 +52,6 @@ resource "google_compute_address" "internal" {
subnetwork = each.value.subnetwork subnetwork = each.value.subnetwork
} }
resource "google_compute_global_address" "psc" {
for_each = var.psc_addresses
project = var.project_id
name = coalesce(each.value.name, each.key)
description = each.value.description
address = try(each.value.address, null)
address_type = "INTERNAL"
network = each.value.network
purpose = "PRIVATE_SERVICE_CONNECT"
# labels = lookup(var.internal_address_labels, each.key, {})
}
resource "google_compute_global_address" "psa" { resource "google_compute_global_address" "psa" {
for_each = var.psa_addresses for_each = var.psa_addresses
project = var.project_id project = var.project_id
@@ -104,17 +78,3 @@ resource "google_compute_address" "ipsec_interconnect" {
purpose = "IPSEC_INTERCONNECT" purpose = "IPSEC_INTERCONNECT"
} }
resource "google_compute_network_attachment" "default" {
provider = google-beta
for_each = local.network_attachments
project = var.project_id
region = each.value.region
name = each.key
description = each.value.description
connection_preference = (
each.value.automatic_connection ? "ACCEPT_AUTOMATIC" : "ACCEPT_MANUAL"
)
subnetworks = [each.value.subnet_self_link]
producer_accept_lists = each.value.producer_accept_lists
producer_reject_lists = each.value.producer_reject_lists
}

102
modules/net-address/psc.tf Normal file
View File

@@ -0,0 +1,102 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
network_attachments = {
for k, v in var.network_attachments : k => merge(v, {
region = regex("regions/([^/]+)", v.subnet_self_link)[0]
# not using the full self link generates a permadiff
subnet_self_link = (
startswith(v.subnet_self_link, "https://")
? v.subnet_self_link
: "https://www.googleapis.com/compute/v1/${v.subnet_self_link}"
)
})
}
regional_psc = {
for name, psc in var.psc_addresses : name => psc if psc.region != null
}
global_psc = {
for name, psc in var.psc_addresses : name => psc if psc.region == null
}
}
resource "google_compute_network_attachment" "default" {
provider = google-beta
for_each = local.network_attachments
project = var.project_id
region = each.value.region
name = each.key
description = each.value.description
connection_preference = (
each.value.automatic_connection ? "ACCEPT_AUTOMATIC" : "ACCEPT_MANUAL"
)
subnetworks = [each.value.subnet_self_link]
producer_accept_lists = each.value.producer_accept_lists
producer_reject_lists = each.value.producer_reject_lists
}
# global PSC services
resource "google_compute_global_address" "psc" {
for_each = local.global_psc
project = var.project_id
name = coalesce(each.value.name, each.key)
description = each.value.description
address = try(each.value.address, null)
address_type = "INTERNAL"
network = each.value.network
purpose = "PRIVATE_SERVICE_CONNECT"
# labels = lookup(var.internal_address_labels, each.key, {})
}
resource "google_compute_global_forwarding_rule" "psc_consumer" {
for_each = { for name, psc in local.global_psc : name => psc if psc.service_attachment != null }
name = coalesce(each.value.name, each.key)
project = var.project_id
subnetwork = each.value.subnet_self_link
ip_address = google_compute_global_address.psc[each.key].self_link
load_balancing_scheme = ""
target = each.value.service_attachment.psc_service_attachment_link
}
# regional PSC services
resource "google_compute_address" "psc" {
for_each = local.regional_psc
project = var.project_id
name = coalesce(each.value.name, each.key)
address = try(each.value.address, null)
address_type = "INTERNAL"
description = each.value.description
network = each.value.network
# purpose not applicable for regional address
# purpose = "PRIVATE_SERVICE_CONNECT"
region = each.value.region
subnetwork = each.value.subnet_self_link
# labels = lookup(var.internal_address_labels, each.key, {})
}
resource "google_compute_forwarding_rule" "psc_consumer" {
for_each = { for name, psc in local.regional_psc : name => psc if psc.service_attachment != null }
name = coalesce(each.value.name, each.key)
project = var.project_id
region = each.value.region
subnetwork = each.value.subnet_self_link
ip_address = google_compute_address.psc[each.key].self_link
load_balancing_scheme = ""
recreate_closed_psc = true
target = each.value.service_attachment.psc_service_attachment_link
}

View File

@@ -107,7 +107,6 @@ variable "psa_addresses" {
prefix_length = number prefix_length = number
description = optional(string, "Terraform managed.") description = optional(string, "Terraform managed.")
name = optional(string) name = optional(string)
})) }))
default = {} default = {}
} }
@@ -115,10 +114,27 @@ variable "psa_addresses" {
variable "psc_addresses" { variable "psc_addresses" {
description = "Map of internal addresses used for Private Service Connect." description = "Map of internal addresses used for Private Service Connect."
type = map(object({ type = map(object({
address = string address = string
network = string description = optional(string, "Terraform managed.")
description = optional(string, "Terraform managed.") name = optional(string)
name = optional(string) network = optional(string)
region = optional(string)
subnet_self_link = optional(string)
service_attachment = optional(object({ # so we can safely check if service_attachemnt != null in for_each
psc_service_attachment_link = string
}))
})) }))
default = {} default = {}
validation {
condition = alltrue([for key, value in var.psc_addresses : (value.region != null || (value.region == null && value.network != null))])
error_message = "Provide network if creating global PSC addresses / endpoints."
}
validation {
condition = alltrue([for key, value in var.psc_addresses : (value.region == null || (value.region != null && value.subnet_self_link != null))])
error_message = "Provide subnet_self_link if creating regional PSC addresses / endpoints."
}
validation {
condition = alltrue([for key, value in var.psc_addresses : !(value.subnet_self_link != null && value.network != null)])
error_message = "Do not provide network and subnet_self_link at the same time"
}
} }

View File

@@ -87,8 +87,15 @@ class FabricTestItem(pytest.Item):
self.extra_files = extra_files self.extra_files = extra_files
def runtest(self): def runtest(self):
s = plan_validator(self.module, self.inventory, self.parent.path.parent, try:
self.tf_var_files, self.extra_files) summary = plan_validator(self.module, self.inventory, self.parent.path.parent,
self.tf_var_files, self.extra_files)
except AssertionError:
def full_paths(x):
return [(self.parent.path.parent / x ) for x in x]
print(f'Error in inventory file: {" ".join(full_paths(self.inventory))}')
print(f'To regenerate inventory run: python tools/plan_summary.py {self.module} {" ".join(full_paths(self.tf_var_files))}')
raise
def reportinfo(self): def reportinfo(self):
return self.path, None, self.name return self.path, None, self.name

View File

@@ -84,7 +84,7 @@ variable "subnet_psc_1" {
name = "subnet_name" name = "subnet_name"
region = "subnet_region" region = "subnet_region"
cidr = "subnet_cidr" cidr = "subnet_cidr"
self_link = "subnet_self_link" self_link = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west8/subnetworks/subnet"
} }
} }

View File

@@ -37,6 +37,12 @@ subnet = {
cidr = "${subnet.ip_cidr_range}" cidr = "${subnet.ip_cidr_range}"
self_link = "${subnet.self_link}" self_link = "${subnet.self_link}"
} }
subnet_psc_1 = {
name = "${subnet_psc_1.name}"
region = "${subnet_psc_1.region}"
cidr = "${subnet_psc_1.ip_cidr_range}"
self_link = "${subnet_psc_1.self_link}"
}
vpc = { vpc = {
name = "${vpc.name}" name = "${vpc.name}"
self_link = "${vpc.self_link}" self_link = "${vpc.self_link}"

View File

@@ -15,7 +15,8 @@
locals { locals {
prefix = "${var.prefix}-${var.timestamp}${var.suffix}" prefix = "${var.prefix}-${var.timestamp}${var.suffix}"
jit_services = [ jit_services = [
"storage.googleapis.com", # no permissions granted by default "storage.googleapis.com", # no permissions granted by default
"sqladmin.googleapis.com", # roles/cloudsql.serviceAgent
] ]
services = [ services = [
# trimmed down list of services, to be extended as needed # trimmed down list of services, to be extended as needed
@@ -35,6 +36,7 @@ locals {
"secretmanager.googleapis.com", "secretmanager.googleapis.com",
"servicenetworking.googleapis.com", "servicenetworking.googleapis.com",
"serviceusage.googleapis.com", "serviceusage.googleapis.com",
"sqladmin.googleapis.com",
"stackdriver.googleapis.com", "stackdriver.googleapis.com",
"storage-component.googleapis.com", "storage-component.googleapis.com",
"storage.googleapis.com", "storage.googleapis.com",
@@ -114,6 +116,36 @@ resource "google_compute_subnetwork" "proxy_only_regional" {
role = "ACTIVE" role = "ACTIVE"
} }
resource "google_compute_subnetwork" "psc" {
project = google_project.project.project_id
network = google_compute_network.network.name
name = "psc-regional"
region = var.region
ip_cidr_range = "10.0.19.0/24"
purpose = "PRIVATE_SERVICE_CONNECT"
}
### PSA ###
resource "google_compute_global_address" "psa_ranges" {
project = google_project.project.project_id
network = google_compute_network.network.id
name = "psa-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
address = "10.0.20.0"
prefix_length = 22
}
resource "google_service_networking_connection" "psa_connection" {
network = google_compute_network.network.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.psa_ranges.name]
deletion_policy = "ABANDON"
}
### END OF PSA
resource "google_service_account" "service_account" { resource "google_service_account" "service_account" {
account_id = "e2e-service-account" account_id = "e2e-service-account"
project = google_project.project.project_id project = google_project.project.project_id
@@ -141,6 +173,12 @@ resource "google_project_service_identity" "jit_si" {
depends_on = [google_project_service.project_service] depends_on = [google_project_service.project_service]
} }
resource "google_project_iam_binding" "cloudsql_agent" {
members = ["serviceAccount:service-${google_project.project.number}@gcp-sa-cloud-sql.iam.gserviceaccount.com"]
project = google_project.project.project_id
role = "roles/cloudsql.serviceAgent"
depends_on = [google_project_service_identity.jit_si]
}
resource "local_file" "terraform_tfvars" { resource "local_file" "terraform_tfvars" {
filename = "e2e_tests.tfvars" filename = "e2e_tests.tfvars"
@@ -168,6 +206,12 @@ resource "local_file" "terraform_tfvars" {
ip_cidr_range = google_compute_subnetwork.subnetwork.ip_cidr_range ip_cidr_range = google_compute_subnetwork.subnetwork.ip_cidr_range
self_link = google_compute_subnetwork.subnetwork.self_link self_link = google_compute_subnetwork.subnetwork.self_link
} }
subnet_psc_1 = {
name = google_compute_subnetwork.psc.name
region = google_compute_subnetwork.psc.region
ip_cidr_range = google_compute_subnetwork.psc.ip_cidr_range
self_link = google_compute_subnetwork.psc.self_link
}
vpc = { vpc = {
name = google_compute_network.network.name name = google_compute_network.network.name
self_link = google_compute_network.network.self_link self_link = google_compute_network.network.self_link

View File

@@ -186,8 +186,13 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None,
# print(yaml.dump({'counts': summary.counts})) # print(yaml.dump({'counts': summary.counts}))
if 'values' in inventory: if 'values' in inventory:
validate_plan_object(inventory['values'], summary.values, relative_path, try:
"") validate_plan_object(inventory['values'], summary.values, relative_path,
"")
except AssertionError:
print(f'\n{path}')
print(yaml.dump({'values': summary.values}))
raise
if 'counts' in inventory: if 'counts' in inventory:
try: try:
@@ -199,6 +204,7 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None,
assert plan_count == expected_count, \ assert plan_count == expected_count, \
f'{relative_path}: count of {type_} resources failed. Got {plan_count}, expected {expected_count}' f'{relative_path}: count of {type_} resources failed. Got {plan_count}, expected {expected_count}'
except AssertionError: except AssertionError:
print(f'\n{path}')
print(yaml.dump({'counts': summary.counts})) print(yaml.dump({'counts': summary.counts}))
raise raise
@@ -218,6 +224,7 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None,
f'{relative_path}: output {output_name} failed. Got `{plan_output}`, expected `{expected_output}`' f'{relative_path}: output {output_name} failed. Got `{plan_output}`, expected `{expected_output}`'
except AssertionError: except AssertionError:
if _buffer: if _buffer:
print(f'\n{path}')
print(yaml.dump(_buffer)) print(yaml.dump(_buffer))
raise raise
return summary return summary

33
tests/fixtures/cloudsql-instance.tf vendored Normal file
View File

@@ -0,0 +1,33 @@
/**
* 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.
*/
module "cloudsql-instance" {
source = "./fabric/modules/cloudsql-instance"
project_id = var.project_id
network_config = {
connectivity = {
psc_allowed_consumer_projects = [var.project_id]
}
}
## define a consumer project with an endpoint within the project
name = "db"
region = var.region
availability_type = "REGIONAL"
database_version = "POSTGRES_13"
tier = "db-g1-small"
gcp_deletion_protection = false
terraform_deletion_protection = false
}

View File

@@ -0,0 +1,23 @@
/**
* 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.
*/
resource "google_kms_crypto_key_iam_binding" "encrypt_decrypt" {
crypto_key_id = var.kms_key.id
members = [
"serviceAccount:service-${var.project_number}@gcp-sa-cloud-sql.iam.gserviceaccount.com"
]
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
}

View File

@@ -17,11 +17,11 @@ values:
database_version: POSTGRES_13 database_version: POSTGRES_13
name: db name: db
project: project-id project: project-id
region: europe-west1 region: europe-west8
settings: settings:
- activation_policy: ALWAYS - activation_policy: ALWAYS
availability_type: ZONAL availability_type: ZONAL
deletion_protection_enabled: true deletion_protection_enabled: false
disk_autoresize: true disk_autoresize: true
disk_type: PD_SSD disk_type: PD_SSD
insights_config: insights_config:

View File

@@ -0,0 +1,38 @@
# 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.db.google_sql_database_instance.primary:
database_version: POSTGRES_13
deletion_protection: false
name: myprefix-db
project: project-id
region: europe-west8
settings:
- activation_policy: ALWAYS
availability_type: REGIONAL
deletion_protection_enabled: false
ip_configuration:
- ipv4_enabled: false
private_network: null
psc_config:
- allowed_consumer_projects:
- project-id
psc_enabled: true
tier: db-g1-small
counts:
google_sql_database_instance: 1
modules: 1
resources: 1

View File

@@ -17,30 +17,11 @@ values:
database_version: MYSQL_8_0 database_version: MYSQL_8_0
name: db name: db
project: project-id project: project-id
region: europe-west1 region: europe-west8
settings: settings:
- activation_policy: ALWAYS - activation_policy: ALWAYS
availability_type: ZONAL availability_type: ZONAL
deletion_protection_enabled: true deletion_protection_enabled: false
disk_autoresize: true
disk_type: PD_SSD
insights_config: []
ip_configuration:
- allocated_ip_range: null
authorized_networks: []
ipv4_enabled: true
private_network: projects/xxx/global/networks/aaa
tier: db-g1-small
module.db.google_sql_database_instance.replicas["replica1"]:
database_version: MYSQL_8_0
master_instance_name: db
name: replica1
project: project-id
region: europe-west3
settings:
- activation_policy: ALWAYS
availability_type: ZONAL
deletion_protection_enabled: true
disk_autoresize: true disk_autoresize: true
disk_type: PD_SSD disk_type: PD_SSD
insights_config: [] insights_config: []
@@ -52,4 +33,4 @@ values:
tier: db-g1-small tier: db-g1-small
counts: counts:
google_sql_database_instance: 2 google_sql_database_instance: 1

View File

@@ -18,7 +18,7 @@ values:
database_version: POSTGRES_13 database_version: POSTGRES_13
name: myprefix-db name: myprefix-db
project: project-id project: project-id
region: europe-west1 region: europe-west8
module.db.google_sql_database_instance.replicas["replica1"]: module.db.google_sql_database_instance.replicas["replica1"]:
clone: [] clone: []
database_version: POSTGRES_13 database_version: POSTGRES_13

View File

@@ -16,10 +16,10 @@ values:
module.db.google_sql_database_instance.primary: module.db.google_sql_database_instance.primary:
clone: [] clone: []
database_version: POSTGRES_13 database_version: POSTGRES_13
deletion_protection: true deletion_protection: false
name: db name: db
project: my-db-project project: test-db-prj
region: europe-west1 region: europe-west8
restore_backup_context: [] restore_backup_context: []
root_password: null root_password: null
settings: settings:
@@ -30,7 +30,7 @@ values:
collation: null collation: null
data_cache_config: [] data_cache_config: []
database_flags: [] database_flags: []
deletion_protection_enabled: true deletion_protection_enabled: false
deny_maintenance_period: [] deny_maintenance_period: []
disk_autoresize: true disk_autoresize: true
disk_autoresize_limit: 0 disk_autoresize_limit: 0
@@ -54,25 +54,25 @@ values:
module.project.google_project.project[0]: module.project.google_project.project[0]:
auto_create_network: false auto_create_network: false
billing_account: 123456-123456-123456 billing_account: 123456-123456-123456
folder_id: null folder_id: '1122334455'
labels: null labels: null
name: my-db-project name: test-db-prj
org_id: '1122334455' org_id: null
project_id: my-db-project project_id: test-db-prj
skip_delete: false skip_delete: false
timeouts: null timeouts: null
module.project.google_project_iam_member.servicenetworking[0]: module.project.google_project_iam_member.servicenetworking[0]:
condition: [] condition: []
project: my-db-project project: test-db-prj
role: roles/servicenetworking.serviceAgent role: roles/servicenetworking.serviceAgent
module.project.google_project_service.project_services["servicenetworking.googleapis.com"]: module.project.google_project_service.project_services["servicenetworking.googleapis.com"]:
disable_dependent_services: false disable_dependent_services: false
disable_on_destroy: false disable_on_destroy: false
project: my-db-project project: test-db-prj
service: servicenetworking.googleapis.com service: servicenetworking.googleapis.com
timeouts: null timeouts: null
module.project.google_project_service_identity.servicenetworking[0]: module.project.google_project_service_identity.servicenetworking[0]:
project: my-db-project project: test-db-prj
service: servicenetworking.googleapis.com service: servicenetworking.googleapis.com
timeouts: null timeouts: null
module.vpc.google_compute_global_address.psa_ranges["servicenetworking-googleapis-com-cloud-sql"]: module.vpc.google_compute_global_address.psa_ranges["servicenetworking-googleapis-com-cloud-sql"]:
@@ -82,7 +82,7 @@ values:
ip_version: null ip_version: null
name: servicenetworking-googleapis-com-cloud-sql name: servicenetworking-googleapis-com-cloud-sql
prefix_length: 16 prefix_length: 16
project: my-db-project project: test-db-prj
purpose: VPC_PEERING purpose: VPC_PEERING
timeouts: null timeouts: null
module.vpc.google_compute_network.network[0]: module.vpc.google_compute_network.network[0]:
@@ -92,14 +92,14 @@ values:
enable_ula_internal_ipv6: null enable_ula_internal_ipv6: null
name: my-network name: my-network
network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL
project: my-db-project project: test-db-prj
routing_mode: GLOBAL routing_mode: GLOBAL
timeouts: null timeouts: null
module.vpc.google_compute_network_peering_routes_config.psa_routes["servicenetworking.googleapis.com"]: module.vpc.google_compute_network_peering_routes_config.psa_routes["servicenetworking.googleapis.com"]:
export_custom_routes: false export_custom_routes: false
import_custom_routes: false import_custom_routes: false
network: my-network network: my-network
project: my-db-project project: test-db-prj
timeouts: null timeouts: null
module.vpc.google_compute_route.gateway["private-googleapis"]: module.vpc.google_compute_route.gateway["private-googleapis"]:
description: Terraform-managed. description: Terraform-managed.
@@ -111,7 +111,7 @@ values:
next_hop_instance: null next_hop_instance: null
next_hop_vpn_tunnel: null next_hop_vpn_tunnel: null
priority: 1000 priority: 1000
project: my-db-project project: test-db-prj
tags: null tags: null
timeouts: null timeouts: null
module.vpc.google_compute_route.gateway["restricted-googleapis"]: module.vpc.google_compute_route.gateway["restricted-googleapis"]:
@@ -124,11 +124,11 @@ values:
next_hop_instance: null next_hop_instance: null
next_hop_vpn_tunnel: null next_hop_vpn_tunnel: null
priority: 1000 priority: 1000
project: my-db-project project: test-db-prj
tags: null tags: null
timeouts: null timeouts: null
module.vpc.google_service_networking_connection.psa_connection["servicenetworking.googleapis.com"]: module.vpc.google_service_networking_connection.psa_connection["servicenetworking.googleapis.com"]:
deletion_policy: null deletion_policy: ABANDON
reserved_peering_ranges: reserved_peering_ranges:
- servicenetworking-googleapis-com-cloud-sql - servicenetworking-googleapis-com-cloud-sql
service: servicenetworking.googleapis.com service: servicenetworking.googleapis.com
@@ -141,11 +141,11 @@ counts:
google_compute_route: 2 google_compute_route: 2
google_project: 1 google_project: 1
google_project_iam_member: 1 google_project_iam_member: 1
google_project_service: 1 google_project_service: 2
google_project_service_identity: 1 google_project_service_identity: 2
google_service_networking_connection: 1 google_service_networking_connection: 1
google_sql_database_instance: 1 google_sql_database_instance: 1
modules: 3 modules: 3
resources: 11 resources: 14
outputs: {} outputs: {}

View File

@@ -0,0 +1,29 @@
# 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.addresses.google_compute_forwarding_rule.psc_consumer["cloudsql-one"]:
load_balancing_scheme: ''
name: cloudsql-one
project: project-id
recreate_closed_psc: true
region: europe-west8
subnetwork: subnet_self_link
module.addresses.google_compute_address.psc["cloudsql-one"]:
address: 10.0.16.32
address_type: INTERNAL
description: Terraform managed.
name: cloudsql-one
project: project-id
subnetwork: subnet_self_link