diff --git a/modules/net-xlb/backend_services.tf b/modules/net-xlb/backend_services.tf index 7ac818b9..e4746d4e 100644 --- a/modules/net-xlb/backend_services.tf +++ b/modules/net-xlb/backend_services.tf @@ -79,18 +79,14 @@ resource "google_compute_backend_service" "group" { # Otherwise, look in the health_checks_config map. # Otherwise, use the health_check id as is (already existing). health_checks = ( - try(each.value.group_config.health_checks, null) == null - || length(try(each.value.group_config.health_checks, [])) == 0 + try(length(each.value.group_config.health_checks), 0) == 0 ? try( - [google_compute_health_check.health_check["default"].self_link], + [google_compute_health_check.health_check["default"].id], null ) : [ for hc in each.value.group_config.health_checks : - try( - google_compute_health_check.health_check[hc].id, - hc - ) + try(google_compute_health_check.health_check[hc].id, hc) ] ) diff --git a/modules/net-xlb/global_forwarding_rule.tf b/modules/net-xlb/global_forwarding_rule.tf index bac4cdab..6fb12ff4 100644 --- a/modules/net-xlb/global_forwarding_rule.tf +++ b/modules/net-xlb/global_forwarding_rule.tf @@ -21,6 +21,11 @@ locals { : null ) + port_range = coalesce( + var.global_forwarding_rule_config.port_range, + var.https ? "443" : "80" + ) + target = ( var.https ? google_compute_target_https_proxy.https.0.id @@ -35,7 +40,7 @@ resource "google_compute_global_forwarding_rule" "forwarding_rule" { description = "Terraform managed." ip_protocol = var.global_forwarding_rule_config.ip_protocol load_balancing_scheme = var.global_forwarding_rule_config.load_balancing_scheme - port_range = var.global_forwarding_rule_config.port_range + port_range = local.port_range target = local.target ip_address = local.ip_address } diff --git a/modules/net-xlb/ssl_certificates.tf b/modules/net-xlb/ssl_certificates.tf index 1f1dc2b8..d3559e7f 100644 --- a/modules/net-xlb/ssl_certificates.tf +++ b/modules/net-xlb/ssl_certificates.tf @@ -15,18 +15,27 @@ */ locals { - managed = ( + # If the HTTPS target proxy has no SSL certs + # set, create also a default managed SSL cert + ssl_certificates_config = merge( + coalesce(var.ssl_certificates_config, {}), + try(length(var.target_proxy_https_config.ssl_certificates), 0) == 0 + ? { default = var.ssl_certificates_config_defaults } + : {} + ) + + ssl_certs_managed = ( var.https ? { - for k, v in coalesce(var.ssl_certificates_config, {}) : + for k, v in coalesce(local.ssl_certificates_config, {}) : k => v if v.unmanaged_config == null } : {} ) - unmanaged = ( + ssl_certs_unmanaged = ( var.https ? { - for k, v in coalesce(var.ssl_certificates_config, {}) : + for k, v in coalesce(local.ssl_certificates_config, {}) : k => v if v.unmanaged_config != null } : {} @@ -34,7 +43,7 @@ locals { } resource "google_compute_managed_ssl_certificate" "managed" { - for_each = local.managed + for_each = local.ssl_certs_managed project = var.project_id name = "${var.name}-${each.key}" managed { @@ -43,7 +52,7 @@ resource "google_compute_managed_ssl_certificate" "managed" { } resource "google_compute_ssl_certificate" "unmanaged" { - for_each = local.unmanaged + for_each = local.ssl_certs_unmanaged project = var.project_id name = "${var.name}-${each.key}" certificate = try(each.value.unmanaged_config.tls_self_signed_cert, null) diff --git a/modules/net-xlb/target_proxy.tf b/modules/net-xlb/target_proxy.tf index dc9f9687..7c4b00c1 100644 --- a/modules/net-xlb/target_proxy.tf +++ b/modules/net-xlb/target_proxy.tf @@ -19,16 +19,21 @@ locals { # Otherwise, look in the ssl_certificates_config map. # Otherwise, use the SSL certificate id as is (already existing). ssl_certificates = ( - var.https == true - ? [ - for cert in try(var.target_proxy_https_config.ssl_certificates, ["default"]) : + try(var.target_proxy_https_config.ssl_certificates, null) == null + || length(coalesce(try(var.target_proxy_https_config.ssl_certificates, null), [])) == 0 + ? try( + [google_compute_managed_ssl_certificate.managed["default"].id], + [google_compute_ssl_certificate.unmanaged["default"].id], + null + ) + : [ + for cert in try(var.target_proxy_https_config.ssl_certificates, []) : try( google_compute_managed_ssl_certificate.managed[cert].id, google_compute_ssl_certificate.unmanaged[cert].id, cert ) ] - : [] ) } diff --git a/modules/net-xlb/variables.tf b/modules/net-xlb/variables.tf index 1d7050f9..a53c113b 100644 --- a/modules/net-xlb/variables.tf +++ b/modules/net-xlb/variables.tf @@ -172,11 +172,22 @@ variable "ssl_certificates_config" { tls_self_signed_cert = string }) })) + default = {} +} + +variable "ssl_certificates_config_defaults" { + description = "The SSL certificate default configuration." + type = object({ + domains = list(string) + # If unmanaged_config is null, the certificate will be managed + unmanaged_config = object({ + tls_private_key = string + tls_self_signed_cert = string + }) + }) default = { - default = { - domains = ["example.com"], - unmanaged_config = null - } + domains = ["example.com"], + unmanaged_config = null } } @@ -201,7 +212,8 @@ variable "global_forwarding_rule_config" { load_balancing_scheme = "EXTERNAL" ip_protocol = "TCP" ip_version = "IPV4" - port_range = "80" # 80, 8080, 443 + # If not specified, 80 for https = false, 443 otherwise + port_range = null } } diff --git a/tests/modules/net_xlb/__init__.py b/tests/modules/net_xlb/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/modules/net_xlb/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/modules/net_xlb/fixture/main.tf b/tests/modules/net_xlb/fixture/main.tf new file mode 100644 index 00000000..d8b97e45 --- /dev/null +++ b/tests/modules/net_xlb/fixture/main.tf @@ -0,0 +1,31 @@ +/** + * 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. + */ + +module "test" { + source = "../../../../modules/net-xlb" + project_id = "my-project" + name = "xlb-test" + health_checks_config_defaults = var.health_checks_config_defaults + health_checks_config = var.health_checks_config + backend_services_config = var.backend_services_config + url_map_config = var.url_map_config + https = var.https + ssl_certificates_config = var.ssl_certificates_config + ssl_certificates_config_defaults = var.ssl_certificates_config_defaults + target_proxy_https_config = var.target_proxy_https_config + reserve_ip_address = var.reserve_ip_address + global_forwarding_rule_config = var.global_forwarding_rule_config +} diff --git a/tests/modules/net_xlb/fixture/variables.tf b/tests/modules/net_xlb/fixture/variables.tf new file mode 100644 index 00000000..0067cce5 --- /dev/null +++ b/tests/modules/net_xlb/fixture/variables.tf @@ -0,0 +1,220 @@ +/** + * 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. + */ + +variable "health_checks_config_defaults" { + description = "Auto-created health check default configuration." + type = object({ + type = string # http https tcp ssl http2 + check = map(any) # actual health check block attributes + options = map(number) # interval, thresholds, timeout + logging = bool + }) + default = { + type = "http" + logging = false + options = {} + check = { + port_specification = "USE_SERVING_PORT" + } + } +} + +variable "health_checks_config" { + description = "Custom health checks configuration." + type = map(object({ + type = string # http https tcp ssl http2 + check = map(any) # actual health check block attributes + options = map(number) # interval, thresholds, timeout + logging = bool + })) + default = {} +} + +variable "backend_services_config" { + description = "The backends services configuration." + type = map(object({ + enable_cdn = bool + + cdn_config = object({ + cache_mode = string + client_ttl = number + default_ttl = number + max_ttl = number + negative_caching = bool + negative_caching_policy = map(number) + serve_while_stale = bool + signed_url_cache_max_age_sec = string + }) + + bucket_config = object({ + bucket_name = string + options = object({ + custom_response_headers = list(string) + }) + }) + + group_config = object({ + backends = list(object({ + group = string # IG or NEG FQDN address + options = object({ + balancing_mode = string # Can be UTILIZATION, RATE, CONNECTION + capacity_scaler = number # Valid range is [0.0,1.0] + max_connections = number + max_connections_per_instance = number + max_connections_per_endpoint = number + max_rate = number + max_rate_per_instance = number + max_rate_per_endpoint = number + max_utilization = number + }) + })) + + # Optional health check ids for backend service groups. + # Will lookup for ids in health_chacks_config first, + # then will use the id as is. If no ids are defined + # at all (null, []) health_checks_config_defaults is used + health_checks = list(string) + + log_config = object({ + enable = bool + sample_rate = number # must be in [0, 1] + }) + + options = object({ + affinity_cookie_ttl_sec = number + custom_request_headers = list(string) + custom_response_headers = list(string) + connection_draining_timeout_sec = number + load_balancing_scheme = string # only EXTERNAL (default) makes sense here + locality_lb_policy = string + port_name = string + protocol = string + security_policy = string + session_affinity = string + timeout_sec = number + + circuits_breakers = object({ + max_requests_per_connection = number # Set to 1 to disable keep-alive + max_connections = number # Defaults to 1024 + max_pending_requests = number # Defaults to 1024 + max_requests = number # Defaults to 1024 + max_retries = number # Defaults to 3 + }) + + consistent_hash = object({ + http_header_name = string + minimum_ring_size = string + http_cookie = object({ + name = string + path = string + ttl = object({ + seconds = number + nanos = number + }) + }) + }) + + iap = object({ + oauth2_client_id = string + oauth2_client_secret = string + oauth2_client_secret_sha256 = string + }) + }) + }) + })) + default = {} +} + +variable "url_map_config" { + description = "The url-map configuration." + type = object({ + default_service = string + default_route_action = any + default_url_redirect = map(any) + header_action = any + host_rules = list(any) + path_matchers = list(any) + tests = list(map(string)) + }) + default = null +} + +variable "ssl_certificates_config" { + description = "The SSL certificate configuration." + type = map(object({ + domains = list(string) + # If unmanaged_config is null, the certificate will be managed + unmanaged_config = object({ + tls_private_key = string + tls_self_signed_cert = string + }) + })) + default = {} +} + +variable "ssl_certificates_config_defaults" { + description = "The SSL certificate default configuration." + type = object({ + domains = list(string) + # If unmanaged_config is null, the certificate will be managed + unmanaged_config = object({ + tls_private_key = string + tls_self_signed_cert = string + }) + }) + default = { + domains = ["example.com"], + unmanaged_config = null + } +} + +variable "target_proxy_https_config" { + description = "The HTTPS target proxy configuration." + type = object({ + ssl_certificates = list(string) + }) + default = null +} + +variable "global_forwarding_rule_config" { + description = "Global forwarding rule configurations." + type = object({ + ip_protocol = string + ip_version = string + load_balancing_scheme = string + port_range = string + + }) + default = { + load_balancing_scheme = "EXTERNAL" + ip_protocol = "TCP" + ip_version = "IPV4" + # If not specified, 80 for https = false, 443 otherwise + port_range = null + } +} + +variable "https" { + description = "Whether to enable HTTPS." + type = bool + default = false +} + +variable "reserve_ip_address" { + description = "Whether to reserve a static global IP address." + type = bool + default = false +} diff --git a/tests/modules/net_xlb/test_plan.py b/tests/modules/net_xlb/test_plan.py new file mode 100644 index 00000000..471d6265 --- /dev/null +++ b/tests/modules/net_xlb/test_plan.py @@ -0,0 +1,245 @@ +# 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. + + +_BACKEND_BUCKET = '''{ + my-bucket = { + bucket_config = { + bucket_name = "my_bucket" + options = null + } + group_config = null + enable_cdn = false + cdn_config = null + } +}''' + +_BACKEND_GROUP = '''{ + my-group = { + bucket_config = null, + enable_cdn = false, + cdn_config = null, + group_config = { + backends = [ + { + group = "my_group", + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + } +}''' + +_BACKEND_GROUP_HC = '''{ + my-group = { + bucket_config = null, + enable_cdn = false, + cdn_config = null, + group_config = { + backends = [ + { + group = "my_group", + options = null + } + ], + health_checks = ["hc_1"] + log_config = null + options = null + } + } +}''' + +_NAME = 'xlb-test' + +_SSL_CERTIFICATES_CONFIG_MANAGED = '''{ + my-domain = { + domains = [ + "my-domain.com" + ] + unmanaged_config = null + } +}''' + +_SSL_CERTIFICATES_CONFIG_UNMANAGED = '''{ + my-domain = { + domains = [ + "my-domain.com" + ], + unmanaged_config = { + tls_private_key = "my-key" + tls_self_signed_cert = "my-cert" + } + } +}''' + +_TARGET_PROXY_HTTPS_CONFIG = '''{ + ssl_certificates = [ + "my-domain" + ] +}''' + + +def test_bucket(plan_runner): + "Tests a bucket backend service." + _, resources = plan_runner(backend_services_config=_BACKEND_BUCKET) + assert len(resources) == 4 + resources = dict((r['type'], r['values']) for r in resources) + + fwd_rule = resources['google_compute_global_forwarding_rule'] + assert fwd_rule['load_balancing_scheme'] == 'EXTERNAL' + assert fwd_rule['port_range'] == '80' + assert fwd_rule['ip_protocol'] == 'TCP' + + bucket = resources['google_compute_backend_bucket'] + assert bucket['name'] == _NAME + '-my-bucket' + assert bucket['enable_cdn'] is False + + assert 'google_compute_health_check' not in resources + assert 'google_compute_target_http_proxy' in resources + assert 'google_compute_url_map' in resources + + +def test_group_default_hc(plan_runner): + "Tests a group backend service with no HC specified." + _, resources = plan_runner(backend_services_config=_BACKEND_GROUP) + assert len(resources) == 5 + resources = dict((r['type'], r['values']) for r in resources) + + fwd_rule = resources['google_compute_global_forwarding_rule'] + assert fwd_rule['load_balancing_scheme'] == 'EXTERNAL' + assert fwd_rule['port_range'] == '80' + assert fwd_rule['ip_protocol'] == 'TCP' + + group = resources['google_compute_backend_service'] + assert len(group['backend']) == 1 + assert group['backend'][0]['group'] == 'my_group' + + health_check = resources['google_compute_health_check'] + assert health_check['name'] == _NAME + '-default' + assert len(health_check['http_health_check']) > 0 + assert len(health_check['https_health_check']) == 0 + assert len(health_check['http2_health_check']) == 0 + assert len(health_check['tcp_health_check']) == 0 + assert health_check['http_health_check'][0]['port_specification'] == 'USE_SERVING_PORT' + assert health_check['http_health_check'][0]['proxy_header'] == 'NONE' + assert health_check['http_health_check'][0]['request_path'] == '/' + + assert 'google_compute_target_http_proxy' in resources + assert 'google_compute_target_https_proxy' not in resources + assert 'google_compute_url_map' in resources + + +def test_group_existing_hc(plan_runner): + "Tests a group backend service with referencing an existing HC." + _, resources = plan_runner(backend_services_config=_BACKEND_GROUP_HC) + assert len(resources) == 4 + resources = dict((r['type'], r['values']) for r in resources) + + assert 'google_compute_backend_service' in resources + assert 'google_compute_global_forwarding_rule' in resources + assert 'google_compute_health_check' not in resources + assert 'google_compute_target_http_proxy' in resources + assert 'google_compute_target_https_proxy' not in resources + assert 'google_compute_url_map' in resources + + +def test_reserved_ip(plan_runner): + "Tests an IP reservation with a group backend service." + _, resources = plan_runner( + backend_services_config=_BACKEND_GROUP, + reserve_ip_address="true" + ) + assert len(resources) == 6 + resources = dict((r['type'], r['values']) for r in resources) + + assert 'google_compute_backend_service' in resources + assert 'google_compute_global_address' in resources + assert 'google_compute_global_forwarding_rule' in resources + assert 'google_compute_target_http_proxy' in resources + assert 'google_compute_target_https_proxy' not in resources + assert 'google_compute_url_map' in resources + + +def test_ssl_managed(plan_runner): + "Tests HTTPS and SSL managed certificates." + _, resources = plan_runner( + backend_services_config=_BACKEND_GROUP, + https='true', + ssl_certificates_config=_SSL_CERTIFICATES_CONFIG_MANAGED, + target_proxy_https_config=_TARGET_PROXY_HTTPS_CONFIG + ) + assert len(resources) == 6 + resources = dict((r['type'], r['values']) for r in resources) + + fwd_rule = resources['google_compute_global_forwarding_rule'] + assert fwd_rule['port_range'] == '443' + + ssl_cert = resources['google_compute_managed_ssl_certificate'] + assert ssl_cert['type'] == "MANAGED" + assert ssl_cert['managed'][0]['domains'][0] == 'my-domain.com' + + assert 'google_compute_backend_service' in resources + assert 'google_compute_global_forwarding_rule' in resources + assert 'google_compute_ssl_certificate' not in resources + assert 'google_compute_target_http_proxy' not in resources + assert 'google_compute_target_https_proxy' in resources + assert 'google_compute_url_map' in resources + + +def test_ssl_unmanaged(plan_runner): + "Tests HTTPS and SSL unmanaged certificates." + _, resources = plan_runner( + backend_services_config=_BACKEND_GROUP, + https="true", + ssl_certificates_config=_SSL_CERTIFICATES_CONFIG_UNMANAGED, + target_proxy_https_config=_TARGET_PROXY_HTTPS_CONFIG + ) + assert len(resources) == 6 + resources = dict((r['type'], r['values']) for r in resources) + + fwd_rule = resources['google_compute_global_forwarding_rule'] + assert fwd_rule['port_range'] == '443' + + assert 'google_compute_backend_service' in resources + assert 'google_compute_global_forwarding_rule' in resources + assert 'google_compute_managed_ssl_certificate' not in resources + assert 'google_compute_ssl_certificate' in resources + assert 'google_compute_target_http_proxy' not in resources + assert 'google_compute_target_https_proxy' in resources + assert 'google_compute_url_map' in resources + + +def test_ssl_existing_cert(plan_runner): + "Tests HTTPS and SSL existing certificate." + _, resources = plan_runner( + backend_services_config=_BACKEND_GROUP, + https="true", + target_proxy_https_config=_TARGET_PROXY_HTTPS_CONFIG + ) + assert len(resources) == 5 + resources = dict((r['type'], r['values']) for r in resources) + + fwd_rule = resources['google_compute_global_forwarding_rule'] + assert fwd_rule['port_range'] == '443' + + assert 'google_compute_backend_service' in resources + assert 'google_compute_global_forwarding_rule' in resources + assert 'google_compute_managed_ssl_certificate' not in resources + assert 'google_compute_ssl_certificate' not in resources + assert 'google_compute_target_http_proxy' not in resources + assert 'google_compute_target_https_proxy' in resources + assert 'google_compute_url_map' in resources