Skip to content

Commit

Permalink
CloudSQL PSC Endpoints support
Browse files Browse the repository at this point in the history
  • Loading branch information
wiktorn committed Apr 27, 2024
1 parent d831d32 commit e55ee06
Show file tree
Hide file tree
Showing 28 changed files with 984 additions and 133 deletions.
268 changes: 185 additions & 83 deletions modules/cloudsql-instance/README.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions modules/cloudsql-instance/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ resource "google_sql_database_instance" "primary" {
dynamic "psc_config" {
for_each = var.network_config.connectivity.psc_allowed_consumer_projects != null ? [""] : []
content {
psc_enabled = true
allowed_consumer_projects = var.network_config.connectivity.psc_allowed_consumer_projects
psc_enabled = true
allowed_consumer_projects = toset(concat(
var.network_config.connectivity.psc_allowed_consumer_projects,
keys(var.psc_consumers)
))
}
}
}
Expand Down
112 changes: 112 additions & 0 deletions modules/cloudsql-instance/psc-consumer.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* 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 {
psc_network_configs = {
for project_id, psc_config in var.psc_consumers :
project_id => merge(
psc_config,
{
attachment_project = coalesce(psc_config.attachment_project, project_id)
}
) if psc_config != null
}
# this has to be separate from psc_network_configs to avoid cyclic dependencies
psc_address_self_links = {
for project_id, psc_config in local.psc_network_configs :
project_id => (
psc_config.create_address ?
google_compute_address.psc_address[project_id].self_link
: psc_config.address
)
}

# Replicas support
# Endpoint for primary instance is separated from replicas endpoints to allow
# easy copy of the PSC endpoint code between modules
replicas_psc_network_configs = merge([
for replica_name, replica_config in var.replicas :
{
for project_id, psc_config in coalesce(replica_config.psc_consumers, {}) :
"${replica_name}-${project_id}" => merge(
psc_config,
{
attachment_project = coalesce(psc_config.attachment_project, project_id)
region = replica_config.region
replica_name = replica_name
}
) if psc_config != null
}
]...)
replicas_psc_address_self_links = {
for key, psc_config in local.replicas_psc_network_configs :
key => (
psc_config.create_address ?
google_compute_address.replica_psc_address[key].self_link
: psc_config.address
)
}
}


resource "google_compute_address" "psc_address" {
for_each = {
for project_id, psc_config in local.psc_network_configs : project_id => psc_config if psc_config.create_address
}
name = coalesce(each.value.address, "psc-cloudsql-primary-${var.name}-${each.key}")
project = each.value.attachment_project
region = var.region
address_type = "INTERNAL"
subnetwork = each.value.subnet_self_link
address = each.value.ipv4_address
}

resource "google_compute_forwarding_rule" "main" {
for_each = local.psc_network_configs
name = "psc-cloudsql-primary-${var.name}-${each.key}"
project = each.value.attachment_project
region = var.region
subnetwork = each.value.subnet_self_link
ip_address = local.psc_address_self_links[each.key]
load_balancing_scheme = ""
target = google_sql_database_instance.primary.psc_service_attachment_link
}


### Replicas support

resource "google_compute_address" "replica_psc_address" {
for_each = {
for key, psc_config in local.replicas_psc_network_configs : key => psc_config if psc_config.create_address
}
name = coalesce(each.value.address, "psc-cloudsql-primary-${var.name}-${each.key}")
project = each.value.attachment_project
region = each.value.region
address_type = "INTERNAL"
subnetwork = each.value.subnet_self_link
address = each.value.ipv4_address
}

resource "google_compute_forwarding_rule" "replicas" {
for_each = local.replicas_psc_network_configs
name = "psc-cloudsql-primary-${var.name}-${each.key}"
project = each.value.attachment_project
region = each.value.region
subnetwork = each.value.subnet_self_link
ip_address = local.replicas_psc_address_self_links[each.key]
load_balancing_scheme = ""
target = google_sql_database_instance.replicas[each.value.replica_name].psc_service_attachment_link
}
38 changes: 38 additions & 0 deletions modules/cloudsql-instance/variables-psc.tf
Original file line number Diff line number Diff line change
@@ -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.
*/

variable "psc_consumers" {
default = {}
description = "Map of PSC Consumers with project_id as a key, and PSC attachment configuration as a value. Use null as value to skip creating a service attachment for this project, but define project as consumer."
nullable = false
type = map(object({
subnet_self_link = string
address = optional(string) # name if create_address == true self_link if create_address == false
attachment_project = optional(string) # defaults to consumer project
create_address = optional(bool, true)
ipv4_address = optional(string)
}))
validation {
error_message = "ipv4_address is allowed only when create_address is true"
condition = alltrue([
for consumer in values(
var.psc_consumers
) : (
try(consumer.ipv4_address, null) == null || try(consumer.create_address, true)
)
])
}
}
19 changes: 18 additions & 1 deletion modules/cloudsql-instance/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,26 @@ variable "replicas" {
description = "Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation."
type = map(object({
region = string
encryption_key_name = string
encryption_key_name = optional(string)
psc_consumers = optional(map(object({
# replica name to PSC configuration
subnet_self_link = string
address = optional(string)
attachment_project = optional(string) # defaults to consumer project
create_address = optional(bool, true)
ipv4_address = optional(string)
})))
}))
default = {}
validation {
error_message = "ipv4_address is allowed only when create_address is true"
condition = alltrue([
for consumer in flatten(
[for c in values(var.replicas) : values(c.psc_consumers) if c.psc_consumers != null]
) : (
try(consumer.ipv4_address, null) == null || try(consumer.create_address, true))
])
}
}

variable "root_password" {
Expand Down
8 changes: 6 additions & 2 deletions tests/collectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@ def __init__(self, name, parent, module, inventory, tf_var_files,
self.extra_files = extra_files

def runtest(self):
s = plan_validator(self.module, self.inventory, self.parent.path.parent,
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)
print(f'\n{self.inventory}')
print(yaml.dump({'values': summary.values}))
print(yaml.dump({'counts': summary.counts}))
print(yaml.dump({'outputs': summary.outputs}))

def reportinfo(self):
return self.path, None, self.name
Expand Down
39 changes: 38 additions & 1 deletion tests/examples_e2e/setup_module/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
locals {
prefix = "${var.prefix}-${var.timestamp}${var.suffix}"
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 = [
# trimmed down list of services, to be extended as needed
Expand All @@ -35,6 +36,7 @@ locals {
"secretmanager.googleapis.com",
"servicenetworking.googleapis.com",
"serviceusage.googleapis.com",
"sqladmin.googleapis.com",
"stackdriver.googleapis.com",
"storage-component.googleapis.com",
"storage.googleapis.com",
Expand Down Expand Up @@ -114,6 +116,35 @@ resource "google_compute_subnetwork" "proxy_only_regional" {
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]
}

### END OF PSA

resource "google_service_account" "service_account" {
account_id = "e2e-service-account"
project = google_project.project.project_id
Expand Down Expand Up @@ -141,6 +172,12 @@ resource "google_project_service_identity" "jit_si" {
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" {
filename = "e2e_tests.tfvars"
Expand Down
11 changes: 9 additions & 2 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,13 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None,
# print(yaml.dump({'counts': summary.counts}))

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:
try:
Expand All @@ -199,6 +204,7 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None,
assert plan_count == expected_count, \
f'{relative_path}: count of {type_} resources failed. Got {plan_count}, expected {expected_count}'
except AssertionError:
print(f'\n{path}')
print(yaml.dump({'counts': summary.counts}))
raise

Expand All @@ -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}`'
except AssertionError:
if _buffer:
print(f'\n{path}')
print(yaml.dump(_buffer))
raise
return summary
Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/cloudsql-kms-iam-grant.tf
Original file line number Diff line number Diff line change
@@ -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"
}
23 changes: 23 additions & 0 deletions tests/modules/cloudsql_instance/common.tfvars
Original file line number Diff line number Diff line change
@@ -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.

name = "db"
network_config = {
connectivity = {}
}
project_id = "db-project"
region = "europe-west4"
availability_type = "REGIONAL"
database_version = "POSTGRES_13"
tier = "db-g1-small"
4 changes: 2 additions & 2 deletions tests/modules/cloudsql_instance/examples/insights.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ values:
database_version: POSTGRES_13
name: db
project: project-id
region: europe-west1
region: europe-west8
settings:
- activation_policy: ALWAYS
availability_type: ZONAL
deletion_protection_enabled: true
deletion_protection_enabled: false
disk_autoresize: true
disk_type: PD_SSD
insights_config:
Expand Down
Loading

0 comments on commit e55ee06

Please sign in to comment.