From c1d3736b0675567ea8be827a80434e154909f12c Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Thu, 19 Jan 2023 10:35:40 +0100 Subject: [PATCH 01/19] fix destroy in stage 1 outputs (#1099) --- fast/stages/01-resman/outputs.tf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fast/stages/01-resman/outputs.tf b/fast/stages/01-resman/outputs.tf index 9b1a67605..e07df577c 100644 --- a/fast/stages/01-resman/outputs.tf +++ b/fast/stages/01-resman/outputs.tf @@ -76,11 +76,11 @@ locals { data-platform-prod = try(module.branch-dp-prod-folder.0.id, null) gke-dev = try(module.branch-gke-dev-folder.0.id, null) gke-prod = try(module.branch-gke-prod-folder.0.id, null) - networking = module.branch-network-folder.id - networking-dev = module.branch-network-dev-folder.id - networking-prod = module.branch-network-prod-folder.id + networking = try(module.branch-network-folder.id, null) + networking-dev = try(module.branch-network-dev-folder.id, null) + networking-prod = try(module.branch-network-prod-folder.id, null) sandbox = try(module.branch-sandbox-folder.0.id, null) - security = module.branch-security-folder.id + security = try(module.branch-security-folder.id, null) teams = try(module.branch-teams-folder.0.id, null) }, { From de704110c9519fe52ead9e6a8c5873bf281a05e5 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Wed, 4 Jan 2023 09:52:06 +0100 Subject: [PATCH 02/19] Update api-gateway tests --- modules/api-gateway/README.md | 28 +++--- tests/modules/api_gateway/examples/basic.yaml | 42 +++++++++ .../api_gateway/examples/create-sa.yaml | 90 +++++++++++++++++++ .../api_gateway/examples/existing-sa.yaml | 71 +++++++++++++++ tests/modules/api_gateway/fixture/main.tf | 26 ------ .../modules/api_gateway/fixture/variables.tf | 55 ------------ tests/modules/api_gateway/test_plan.py | 19 ---- .../modules/organization/examples/basic.yaml | 2 +- 8 files changed, 218 insertions(+), 115 deletions(-) create mode 100644 tests/modules/api_gateway/examples/basic.yaml create mode 100644 tests/modules/api_gateway/examples/create-sa.yaml create mode 100644 tests/modules/api_gateway/examples/existing-sa.yaml delete mode 100644 tests/modules/api_gateway/fixture/main.tf delete mode 100644 tests/modules/api_gateway/fixture/variables.tf delete mode 100644 tests/modules/api_gateway/test_plan.py diff --git a/modules/api-gateway/README.md b/modules/api-gateway/README.md index 0b5fc9283..d3c16d38c 100644 --- a/modules/api-gateway/README.md +++ b/modules/api-gateway/README.md @@ -1,4 +1,4 @@ -# Api Gateway +# API Gateway This module allows creating an API with its associated API config and API gateway. It also allows you grant IAM roles on the created resources. # Examples @@ -15,46 +15,46 @@ module "gateway" { # ... EOT } -# tftest modules=1 resources=4 +# tftest modules=1 resources=4 inventory=basic.yaml ``` -## Basic example + customer service account +## Use existing service account ```hcl module "gateway" { source = "./fabric/modules/api-gateway" project_id = "my-project" api_id = "api" region = "europe-west1" - spec = < diff --git a/tests/modules/api_gateway/examples/basic.yaml b/tests/modules/api_gateway/examples/basic.yaml new file mode 100644 index 000000000..a17fc3ca4 --- /dev/null +++ b/tests/modules/api_gateway/examples/basic.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 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.gateway.google_api_gateway_api.api: + api_id: api + display_name: api + project: my-project + module.gateway.google_api_gateway_api_config.api_config: + api: api + gateway_config: [] + grpc_services: [] + labels: null + managed_service_configs: [] + project: my-project + module.gateway.google_api_gateway_gateway.gateway: + display_name: gw-api + gateway_id: gw-api + labels: null + project: my-project + region: europe-west1 + module.gateway.google_project_service.service: + disable_dependent_services: true + disable_on_destroy: true + project: my-project + +counts: + google_api_gateway_api: 1 + google_api_gateway_api_config: 1 + google_api_gateway_gateway: 1 + google_project_service: 1 diff --git a/tests/modules/api_gateway/examples/create-sa.yaml b/tests/modules/api_gateway/examples/create-sa.yaml new file mode 100644 index 000000000..2c8d7c763 --- /dev/null +++ b/tests/modules/api_gateway/examples/create-sa.yaml @@ -0,0 +1,90 @@ +# Copyright 2023 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.gateway.google_api_gateway_api.api: + api_id: api + display_name: api + labels: null + project: my-project + module.gateway.google_api_gateway_api_config.api_config: + api: api + grpc_services: [] + labels: null + managed_service_configs: [] + project: my-project + module.gateway.google_api_gateway_api_config_iam_binding.api_config_iam_bindings["roles/apigateway.admin"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_api_config_iam_binding.api_config_iam_bindings["roles/apigateway.viewer"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.viewer + module.gateway.google_api_gateway_api_iam_binding.api_iam_bindings["roles/apigateway.admin"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_api_iam_binding.api_iam_bindings["roles/apigateway.viewer"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.viewer + module.gateway.google_api_gateway_gateway.gateway: + display_name: gw-api + gateway_id: gw-api + labels: null + project: my-project + region: europe-west1 + module.gateway.google_api_gateway_gateway_iam_binding.gateway_iam_bindings["roles/apigateway.admin"]: + condition: [] + gateway: gw-api + members: + - user:mirene@google.com + project: my-project + region: europe-west1 + role: roles/apigateway.admin + module.gateway.google_api_gateway_gateway_iam_binding.gateway_iam_bindings["roles/apigateway.viewer"]: + condition: [] + gateway: gw-api + members: + - user:mirene@google.com + project: my-project + region: europe-west1 + role: roles/apigateway.viewer + module.gateway.google_project_service.service: {} + module.gateway.google_service_account.service_account[0]: + account_id: sa-api-cfg-api + project: my-project + +counts: + google_api_gateway_api: 1 + google_api_gateway_api_config: 1 + google_api_gateway_api_config_iam_binding: 2 + google_api_gateway_api_iam_binding: 2 + google_api_gateway_gateway: 1 + google_api_gateway_gateway_iam_binding: 2 + google_project_service: 1 + google_service_account: 1 diff --git a/tests/modules/api_gateway/examples/existing-sa.yaml b/tests/modules/api_gateway/examples/existing-sa.yaml new file mode 100644 index 000000000..f0befa79a --- /dev/null +++ b/tests/modules/api_gateway/examples/existing-sa.yaml @@ -0,0 +1,71 @@ +# Copyright 2023 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.gateway.google_api_gateway_api.api: + api_id: api + display_name: api + labels: null + project: my-project + module.gateway.google_api_gateway_api_config.api_config: + api: api + gateway_config: + - backend_config: + - google_service_account: sa@my-project.iam.gserviceaccount.com + grpc_services: [] + labels: null + managed_service_configs: [] + project: my-project + module.gateway.google_api_gateway_api_config_iam_binding.api_config_iam_bindings["roles/apigateway.admin"]: + api: api + api_config: api-cfg-api-8656c6040d6d9ba18a8b9b5f3955c223 + condition: [] + members: + - user:user@example.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_api_iam_binding.api_iam_bindings["roles/apigateway.admin"]: + api: api + condition: [] + members: + - user:user@example.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_gateway.gateway: + display_name: gw-api + gateway_id: gw-api + labels: null + project: my-project + region: europe-west1 + module.gateway.google_api_gateway_gateway_iam_binding.gateway_iam_bindings["roles/apigateway.admin"]: + condition: [] + gateway: gw-api + members: + - user:user@example.com + project: my-project + region: europe-west1 + role: roles/apigateway.admin + module.gateway.google_project_service.service: + disable_dependent_services: true + disable_on_destroy: true + project: my-project + +counts: + google_api_gateway_api: 1 + google_api_gateway_api_config: 1 + google_api_gateway_api_config_iam_binding: 1 + google_api_gateway_api_iam_binding: 1 + google_api_gateway_gateway: 1 + google_api_gateway_gateway_iam_binding: 1 + google_project_service: 1 diff --git a/tests/modules/api_gateway/fixture/main.tf b/tests/modules/api_gateway/fixture/main.tf deleted file mode 100644 index d4cd134f2..000000000 --- a/tests/modules/api_gateway/fixture/main.tf +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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 "gateway" { - source = "../../../../modules/api-gateway" - api_id = var.api_id - project_id = var.project_id - labels = var.labels - iam = var.iam - region = var.region - spec = var.spec - service_account_create = true -} diff --git a/tests/modules/api_gateway/fixture/variables.tf b/tests/modules/api_gateway/fixture/variables.tf deleted file mode 100644 index 977af921d..000000000 --- a/tests/modules/api_gateway/fixture/variables.tf +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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 "api_id" { - type = string - default = "my-api" -} - -variable "iam" { - type = map(list(string)) - default = null -} - -variable "labels" { - type = map(string) - default = null -} - -variable "project_id" { - type = string - default = "my-project" -} - -variable "region" { - type = string - default = "europe-west1" -} - -variable "service_account_create" { - type = bool - default = true -} - -variable "service_account_email" { - type = string - default = null -} - -variable "spec" { - type = string - default = "Spec contents" -} diff --git a/tests/modules/api_gateway/test_plan.py b/tests/modules/api_gateway/test_plan.py deleted file mode 100644 index 18ecdd329..000000000 --- a/tests/modules/api_gateway/test_plan.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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. - - -def test_resource_count(plan_runner): - "Test number of resources created." - _, resources = plan_runner() - assert len(resources) == 5 diff --git a/tests/modules/organization/examples/basic.yaml b/tests/modules/organization/examples/basic.yaml index 2ba70f40a..f7b63a1d4 100644 --- a/tests/modules/organization/examples/basic.yaml +++ b/tests/modules/organization/examples/basic.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 4897aa7109becfd592b5fd9efc6f392379489f69 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Wed, 18 Jan 2023 12:18:41 +0100 Subject: [PATCH 03/19] bump test suite versions --- tests/requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index a6f82d750..1e0921c19 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ -pytest>=6.2.5 +pytest>=7.2.1 PyYAML>=6.0 tftest>=1.8.1 -marko>=1.2.0 -deepdiff>=5.7.0 -python-hcl2>=3.0.5 +marko>=1.2.2 +deepdiff>=6.2.3 +python-hcl2>=4.3.0 From 410b7f5ba3c30ac551d90ca9b0ab036f247fde3c Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 00:00:31 +0100 Subject: [PATCH 04/19] Fix typo in net-vpc DNS policies --- modules/net-vpc/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/net-vpc/main.tf b/modules/net-vpc/main.tf index 7eedc95ac..d15058017 100644 --- a/modules/net-vpc/main.tf +++ b/modules/net-vpc/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,7 +109,7 @@ resource "google_dns_policy" "default" { ) iterator = ns content { - ipv4_address = ns.key + ipv4_address = ns.value forwarding_path = "private" } } @@ -121,7 +121,7 @@ resource "google_dns_policy" "default" { ) iterator = ns content { - ipv4_address = ns.key + ipv4_address = ns.value } } } From fd19e4a923205deff9775494c7c8b96d799b3878 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 00:00:58 +0100 Subject: [PATCH 05/19] add inventories net-vpc examples --- modules/net-vpc/README.md | 14 ++--- .../net_vpc/examples/dns-policies.yaml | 42 +++++++++++++++ tests/modules/net_vpc/examples/peering.yaml | 34 +++++++++++++ .../net_vpc/examples/proxy-only-subnets.yaml | 40 +++++++++++++++ .../modules/net_vpc/examples/psc-routes.yaml | 47 +++++++++++++++++ tests/modules/net_vpc/examples/psc.yaml | 46 +++++++++++++++++ .../modules/net_vpc/examples/shared-vpc.yaml | 51 +++++++++++++++++++ tests/modules/net_vpc/examples/simple.yaml | 50 ++++++++++++++++++ 8 files changed, 317 insertions(+), 7 deletions(-) create mode 100644 tests/modules/net_vpc/examples/dns-policies.yaml create mode 100644 tests/modules/net_vpc/examples/peering.yaml create mode 100644 tests/modules/net_vpc/examples/proxy-only-subnets.yaml create mode 100644 tests/modules/net_vpc/examples/psc-routes.yaml create mode 100644 tests/modules/net_vpc/examples/psc.yaml create mode 100644 tests/modules/net_vpc/examples/shared-vpc.yaml create mode 100644 tests/modules/net_vpc/examples/simple.yaml diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 53361009e..71884671e 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -30,7 +30,7 @@ module "vpc" { } ] } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=simple.yaml ``` ### Peering @@ -65,7 +65,7 @@ module "vpc-spoke-1" { import_routes = true } } -# tftest modules=2 resources=6 +# tftest modules=2 resources=6 inventory=peering.yaml ``` ### Shared VPC @@ -116,7 +116,7 @@ module "vpc-host" { } } } -# tftest modules=1 resources=7 +# tftest modules=1 resources=7 inventory=shared-vpc.yaml ``` ### Private Service Networking @@ -137,7 +137,7 @@ module "vpc" { ranges = { myrange = "10.0.1.0/24" } } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=5 inventory=psc.yaml ``` ### Private Service Networking with peering routes @@ -162,7 +162,7 @@ module "vpc" { import_routes = true } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=5 inventory=psc-routes.yaml ``` ### Subnets for Private Service Connect, Proxy-only subnets @@ -194,7 +194,7 @@ module "vpc" { } ] } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=proxy-only-subnets.yaml ``` ### DNS Policies @@ -219,7 +219,7 @@ module "vpc" { } ] } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=dns-policies.yaml ``` ### Subnet Factory diff --git a/tests/modules/net_vpc/examples/dns-policies.yaml b/tests/modules/net_vpc/examples/dns-policies.yaml new file mode 100644 index 000000000..a30d6408a --- /dev/null +++ b/tests/modules/net_vpc/examples/dns-policies.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 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.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: {} + module.vpc.google_dns_policy.default[0]: + alternative_name_server_config: + - target_name_servers: + - forwarding_path: '' + ipv4_address: '8.8.8.8' + - forwarding_path: private + ipv4_address: '10.0.0.1' + description: Managed by Terraform + enable_inbound_forwarding: true + enable_logging: null + name: my-network + networks: + - {} + project: my-project + +counts: + google_compute_network: 1 + google_compute_subnetwork: 1 + google_dns_policy: 1 + modules: 1 + resources: 3 + +outputs: {} diff --git a/tests/modules/net_vpc/examples/peering.yaml b/tests/modules/net_vpc/examples/peering.yaml new file mode 100644 index 000000000..937ce1445 --- /dev/null +++ b/tests/modules/net_vpc/examples/peering.yaml @@ -0,0 +1,34 @@ +# Copyright 2023 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.vpc-hub.google_compute_network.network[0]: {} + module.vpc-spoke-1.google_compute_network.network[0]: {} + module.vpc-hub.google_compute_subnetwork.subnetwork["europe-west1/subnet-1"]: {} + module.vpc-spoke-1.google_compute_subnetwork.subnetwork["europe-west1/subnet-2"]: {} + module.vpc-spoke-1.google_compute_network_peering.local[0]: + export_custom_routes: false + export_subnet_routes_with_public_ip: true + import_custom_routes: true + import_subnet_routes_with_public_ip: null + module.vpc-spoke-1.google_compute_network_peering.remote[0]: + export_custom_routes: true + export_subnet_routes_with_public_ip: true + import_custom_routes: false + import_subnet_routes_with_public_ip: null + +counts: + google_compute_network: 2 + google_compute_network_peering: 2 + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/examples/proxy-only-subnets.yaml b/tests/modules/net_vpc/examples/proxy-only-subnets.yaml new file mode 100644 index 000000000..6e2069aaa --- /dev/null +++ b/tests/modules/net_vpc/examples/proxy-only-subnets.yaml @@ -0,0 +1,40 @@ +# Copyright 2023 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.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.proxy_only["europe-west1/regional-proxy"]: + description: Terraform-managed proxy-only subnet for Regional HTTPS or Internal HTTPS LB. + ip_cidr_range: 10.0.1.0/24 + log_config: [] + name: regional-proxy + project: my-project + purpose: REGIONAL_MANAGED_PROXY + region: europe-west1 + role: ACTIVE + module.vpc.google_compute_subnetwork.psc["europe-west1/psc"]: + description: Terraform-managed subnet for Private Service Connect (PSC NAT). + ip_cidr_range: 10.0.3.0/24 + log_config: [] + name: psc + project: my-project + purpose: PRIVATE_SERVICE_CONNECT + region: europe-west1 + role: null + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/examples/psc-routes.yaml b/tests/modules/net_vpc/examples/psc-routes.yaml new file mode 100644 index 000000000..6f459f4b7 --- /dev/null +++ b/tests/modules/net_vpc/examples/psc-routes.yaml @@ -0,0 +1,47 @@ +# Copyright 2023 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.vpc.google_compute_global_address.psa_ranges["myrange"]: + address: 10.0.1.0 + address_type: INTERNAL + description: null + ip_version: null + name: myrange + prefix_length: 24 + project: my-project + purpose: VPC_PEERING + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + routing_mode: GLOBAL + module.vpc.google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: true + import_custom_routes: true + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: + ip_cidr_range: 10.0.0.0/24 + name: production + project: my-project + module.vpc.google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - myrange + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 1 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_compute_subnetwork: 1 + google_service_networking_connection: 1 diff --git a/tests/modules/net_vpc/examples/psc.yaml b/tests/modules/net_vpc/examples/psc.yaml new file mode 100644 index 000000000..c08fcb453 --- /dev/null +++ b/tests/modules/net_vpc/examples/psc.yaml @@ -0,0 +1,46 @@ +# Copyright 2023 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.vpc.google_compute_global_address.psa_ranges["myrange"]: + address: 10.0.1.0 + address_type: INTERNAL + name: myrange + prefix_length: 24 + project: my-project + purpose: VPC_PEERING + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: false + import_custom_routes: false + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: + ip_cidr_range: 10.0.0.0/24 + name: production + project: my-project + module.vpc.google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - myrange + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 1 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_compute_subnetwork: 1 + google_service_networking_connection: 1 + +outputs: {} diff --git a/tests/modules/net_vpc/examples/shared-vpc.yaml b/tests/modules/net_vpc/examples/shared-vpc.yaml new file mode 100644 index 000000000..b004e3151 --- /dev/null +++ b/tests/modules/net_vpc/examples/shared-vpc.yaml @@ -0,0 +1,51 @@ +# Copyright 2023 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.vpc-host.google_compute_network.network[0]: + name: my-host-network + project: my-project + module.vpc-host.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: my-project + module.vpc-host.google_compute_shared_vpc_service_project.service_projects["project1"]: + host_project: my-project + service_project: project1 + module.vpc-host.google_compute_shared_vpc_service_project.service_projects["project2"]: + host_project: my-project + service_project: project2 + module.vpc-host.google_compute_subnetwork.subnetwork["europe-west1/subnet-1"]: {} + module.vpc-host.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-1.roles/compute.networkUser"]: + condition: [] + members: + - serviceAccount:cloudsvc + - serviceAccount:gke + project: my-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: subnet-1 + module.vpc-host.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-1.roles/compute.securityAdmin"]: + condition: [] + members: + - serviceAccount:gke + project: my-project + region: europe-west1 + role: roles/compute.securityAdmin + subnetwork: subnet-1 + +counts: + google_compute_network: 1 + google_compute_shared_vpc_host_project: 1 + google_compute_shared_vpc_service_project: 2 + google_compute_subnetwork: 1 + google_compute_subnetwork_iam_binding: 2 diff --git a/tests/modules/net_vpc/examples/simple.yaml b/tests/modules/net_vpc/examples/simple.yaml new file mode 100644 index 000000000..799852c02 --- /dev/null +++ b/tests/modules/net_vpc/examples/simple.yaml @@ -0,0 +1,50 @@ +# Copyright 2023 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.vpc.google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + name: my-network + project: my-project + routing_mode: GLOBAL + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: + description: Terraform-managed. + ip_cidr_range: 10.0.0.0/24 + log_config: [] + name: production + private_ip_google_access: true + project: my-project + region: europe-west1 + role: null + secondary_ip_range: + - ip_cidr_range: 172.16.0.0/20 + range_name: pods + - ip_cidr_range: 192.168.0.0/24 + range_name: services + module.vpc.google_compute_subnetwork.subnetwork["europe-west2/production"]: + description: Terraform-managed. + ip_cidr_range: 10.0.16.0/24 + log_config: [] + name: production + private_ip_google_access: true + project: my-project + region: europe-west2 + role: null + secondary_ip_range: [] + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 From 12f07ebeac446e618e5696664556daf0eb4b2392 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 00:29:16 +0100 Subject: [PATCH 06/19] Extend net-vpc README with more tested examples --- modules/net-vpc/README.md | 81 ++++++++++++ tests/examples/test_plan.py | 10 +- .../modules/net_vpc/examples/subnet-iam.yaml | 54 ++++++++ .../net_vpc/examples/subnet-options.yaml | 70 ++++++++++ tests/modules/net_vpc/fixture/main.tf | 30 ----- .../net_vpc/fixture/test.subnets.tfvars | 44 ------- tests/modules/net_vpc/fixture/variables.tf | 101 --------------- tests/modules/net_vpc/peering.tfvars | 5 - tests/modules/net_vpc/peering.yaml | 47 ------- tests/modules/net_vpc/psa_simple.tfvars | 7 - tests/modules/net_vpc/psa_simple.yaml | 70 ---------- tests/modules/net_vpc/simple.tfvars | 1 - tests/modules/net_vpc/simple.yaml | 36 ------ tests/modules/net_vpc/subnets.tfvars | 44 ------- tests/modules/net_vpc/subnets.yaml | 120 ------------------ tests/modules/net_vpc/tftest.yaml | 6 +- 16 files changed, 211 insertions(+), 515 deletions(-) create mode 100644 tests/modules/net_vpc/examples/subnet-iam.yaml create mode 100644 tests/modules/net_vpc/examples/subnet-options.yaml delete mode 100644 tests/modules/net_vpc/fixture/main.tf delete mode 100644 tests/modules/net_vpc/fixture/test.subnets.tfvars delete mode 100644 tests/modules/net_vpc/fixture/variables.tf delete mode 100644 tests/modules/net_vpc/peering.tfvars delete mode 100644 tests/modules/net_vpc/peering.yaml delete mode 100644 tests/modules/net_vpc/psa_simple.tfvars delete mode 100644 tests/modules/net_vpc/psa_simple.yaml delete mode 100644 tests/modules/net_vpc/simple.tfvars delete mode 100644 tests/modules/net_vpc/simple.yaml delete mode 100644 tests/modules/net_vpc/subnets.tfvars delete mode 100644 tests/modules/net_vpc/subnets.yaml diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 71884671e..5a9013780 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -33,6 +33,87 @@ module "vpc" { # tftest modules=1 resources=3 inventory=simple.yaml ``` +### Subnet Options +```hcl +module "vpc" { + source = "./fabric/modules/net-vpc" + project_id = "my-project" + name = "my-network" + subnets = [ + # simple subnet + { + name = "simple" + region = "europe-west1" + ip_cidr_range = "10.0.0.0/24" + }, + # custom description and PGA disabled + { + name = "no-pga" + region = "europe-west1" + ip_cidr_range = "10.0.1.0/24", + description = "Subnet b" + enable_private_access = false + }, + # secondary ranges + { + name = "with-secondary-ranges" + region = "europe-west1" + ip_cidr_range = "10.0.2.0/24" + secondary_ip_ranges = { + a = "192.168.0.0/24" + b = "192.168.1.0/24" + } + }, + # enable flow logs + { + name = "with-flow-logs" + region = "europe-west1" + ip_cidr_range = "10.0.3.0/24" + flow_logs_config = { + flow_sampling = 0.5 + aggregation_interval = "INTERVAL_10_MIN" + } + } + ] +} +# tftest modules=1 resources=5 inventory=subnet-options.yaml +``` + +### Subnet IAM + +```hcl +module "vpc" { + source = "./fabric/modules/net-vpc" + project_id = "my-project" + name = "my-network" + subnets = [ + { + name = "subnet-1" + region = "europe-west1" + ip_cidr_range = "10.0.1.0/24" + }, + { + name = "subnet-2" + region = "europe-west1" + ip_cidr_range = "10.0.1.0/24" + } + ] + subnet_iam = { + "europe-west1/subnet-1" = { + "roles/compute.networkUser" = [ + "user:user1@example.com", "group:group1@example.com" + ] + } + "europe-west1/subnet-2" = { + "roles/compute.networkUser" = [ + "user:user2@example.com", "group:group2@example.com" + ] + } + } +} +# tftest modules=1 resources=5 inventory=subnet-iam.yaml +``` + ### Peering A single peering can be configured for the VPC, so as to allow management of simple scenarios, and more complex configurations like hub and spoke by defining the peering configuration on the spoke VPCs. Care must be taken so as a single peering is created/changed/destroyed at a time, due to the specific behaviour of the peering API calls. diff --git a/tests/examples/test_plan.py b/tests/examples/test_plan.py index 5f902cbe7..2dd42a7ad 100644 --- a/tests/examples/test_plan.py +++ b/tests/examples/test_plan.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,10 +49,10 @@ def test_example(plan_validator, tmp_path, example): summary = plan_validator(module_path=tmp_path, inventory_paths=inventory, tf_var_files=[]) - import yaml - print(yaml.dump({"values": summary.values})) - print(yaml.dump({"counts": summary.counts})) - print(yaml.dump({"outputs": summary.outputs})) + # import yaml + # print(yaml.dump({"values": summary.values})) + # print(yaml.dump({"counts": summary.counts})) + # print(yaml.dump({"outputs": summary.outputs})) counts = summary.counts num_modules, num_resources = counts['modules'], counts['resources'] diff --git a/tests/modules/net_vpc/examples/subnet-iam.yaml b/tests/modules/net_vpc/examples/subnet-iam.yaml new file mode 100644 index 000000000..cb53ecd80 --- /dev/null +++ b/tests/modules/net_vpc/examples/subnet-iam.yaml @@ -0,0 +1,54 @@ +# Copyright 2023 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.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/subnet-1"]: + name: subnet-1 + project: my-project + region: europe-west1 + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/subnet-2"]: + name: subnet-2 + private_ip_google_access: true + project: my-project + region: europe-west1 + module.vpc.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-1.roles/compute.networkUser"]: + condition: [] + members: + - group:group1@example.com + - user:user1@example.com + project: my-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: subnet-1 + module.vpc.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-2.roles/compute.networkUser"]: + condition: [] + members: + - group:group2@example.com + - user:user2@example.com + project: my-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: subnet-2 + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 + google_compute_subnetwork_iam_binding: 2 + modules: 1 + resources: 5 + +outputs: {} diff --git a/tests/modules/net_vpc/examples/subnet-options.yaml b/tests/modules/net_vpc/examples/subnet-options.yaml new file mode 100644 index 000000000..e3cea5ca6 --- /dev/null +++ b/tests/modules/net_vpc/examples/subnet-options.yaml @@ -0,0 +1,70 @@ +# Copyright 2023 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.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/no-pga"]: + description: Subnet b + ip_cidr_range: 10.0.1.0/24 + log_config: [] + name: no-pga + private_ip_google_access: false + project: my-project + region: europe-west1 + secondary_ip_range: [] + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/simple"]: + description: Terraform-managed. + ip_cidr_range: 10.0.0.0/24 + log_config: [] + name: simple + private_ip_google_access: true + project: my-project + region: europe-west1 + secondary_ip_range: [] + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/with-flow-logs"]: + description: Terraform-managed. + ip_cidr_range: 10.0.3.0/24 + ipv6_access_type: null + log_config: + - aggregation_interval: INTERVAL_10_MIN + filter_expr: 'true' + flow_sampling: 0.5 + metadata: INCLUDE_ALL_METADATA + metadata_fields: null + name: with-flow-logs + private_ip_google_access: true + project: my-project + region: europe-west1 + role: null + secondary_ip_range: [] + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/with-secondary-ranges"]: + description: Terraform-managed. + ip_cidr_range: 10.0.2.0/24 + log_config: [] + name: with-secondary-ranges + private_ip_google_access: true + project: my-project + region: europe-west1 + role: null + secondary_ip_range: + - ip_cidr_range: 192.168.0.0/24 + range_name: a + - ip_cidr_range: 192.168.1.0/24 + range_name: b + +counts: + google_compute_network: 1 + google_compute_subnetwork: 4 diff --git a/tests/modules/net_vpc/fixture/main.tf b/tests/modules/net_vpc/fixture/main.tf deleted file mode 100644 index f0e4696e0..000000000 --- a/tests/modules/net_vpc/fixture/main.tf +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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-vpc" - project_id = "test-project" - name = "test" - peering_config = var.peering_config - routes = var.routes - shared_vpc_host = var.shared_vpc_host - shared_vpc_service_projects = var.shared_vpc_service_projects - subnet_iam = var.subnet_iam - subnets = var.subnets - auto_create_subnetworks = var.auto_create_subnetworks - psa_config = var.psa_config - data_folder = var.data_folder -} diff --git a/tests/modules/net_vpc/fixture/test.subnets.tfvars b/tests/modules/net_vpc/fixture/test.subnets.tfvars deleted file mode 100644 index 499e498f4..000000000 --- a/tests/modules/net_vpc/fixture/test.subnets.tfvars +++ /dev/null @@ -1,44 +0,0 @@ -subnet_iam = { - "europe-west1/a" = { - "roles/compute.networkUser" = [ - "user:a@example.com", "group:g-a@example.com" - ] - } - "europe-west1/c" = { - "roles/compute.networkUser" = [ - "user:c@example.com", "group:g-c@example.com" - ] - } -} -subnets = [ - { - name = "a" - region = "europe-west1" - ip_cidr_range = "10.0.0.0/24" - }, - { - name = "b" - region = "europe-west1" - ip_cidr_range = "10.0.1.0/24", - description = "Subnet b" - enable_private_access = false - }, - { - name = "c" - region = "europe-west1" - ip_cidr_range = "10.0.2.0/24" - secondary_ip_ranges = { - a = "192.168.0.0/24" - b = "192.168.1.0/24" - } - }, - { - name = "d" - region = "europe-west1" - ip_cidr_range = "10.0.3.0/24" - flow_logs_config = { - flow_sampling = 0.5 - aggregation_interval = "INTERVAL_10_MIN" - } - } -] diff --git a/tests/modules/net_vpc/fixture/variables.tf b/tests/modules/net_vpc/fixture/variables.tf deleted file mode 100644 index 868966c8b..000000000 --- a/tests/modules/net_vpc/fixture/variables.tf +++ /dev/null @@ -1,101 +0,0 @@ -/** - * 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 "auto_create_subnetworks" { - type = bool - default = false -} - -variable "data_folder" { - type = string - default = null -} - -variable "delete_default_routes_on_create" { - type = bool - default = false -} - -variable "description" { - type = string - default = "Terraform-managed." -} - -variable "dns_policy" { - type = any - default = null -} - -variable "mtu" { - type = number - default = null -} - -variable "peering_config" { - type = any - default = null -} - -variable "psa_config" { - type = any - default = null -} - -variable "routes" { - type = any - default = {} - nullable = false -} - -variable "routing_mode" { - type = string - default = "GLOBAL" -} - -variable "shared_vpc_host" { - type = bool - default = false -} - -variable "shared_vpc_service_projects" { - type = list(string) - default = [] -} - -variable "subnets" { - type = any - default = [] -} - -variable "subnet_iam" { - type = map(map(list(string))) - default = {} -} - -variable "subnets_proxy_only" { - type = any - default = [] -} - -variable "subnets_psc" { - type = any - default = [] -} - -variable "vpc_create" { - type = bool - default = true -} diff --git a/tests/modules/net_vpc/peering.tfvars b/tests/modules/net_vpc/peering.tfvars deleted file mode 100644 index eccd7ae71..000000000 --- a/tests/modules/net_vpc/peering.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -peering_config = { - peer_vpc_self_link = "projects/my-project/global/networks/peer" - export_routes = true - import_routes = null -} diff --git a/tests/modules/net_vpc/peering.yaml b/tests/modules/net_vpc/peering.yaml deleted file mode 100644 index 8d0bbed71..000000000 --- a/tests/modules/net_vpc/peering.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# 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. - -values: - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - name: test - project: test-project - routing_mode: GLOBAL - google_compute_network_peering.local[0]: - export_custom_routes: true - import_custom_routes: false - name: test-peer - peer_network: projects/my-project/global/networks/peer - google_compute_network_peering.remote[0]: - export_custom_routes: false - import_custom_routes: true - name: peer-test - network: projects/my-project/global/networks/peer - -counts: - google_compute_network: 1 - google_compute_network_peering: 2 - -outputs: - bindings: {} - project_id: test-project - subnet_ips: {} - subnet_regions: {} - subnet_secondary_ranges: {} - subnet_self_links: {} - subnets: {} - subnets_proxy_only: {} - subnets_psc: {} diff --git a/tests/modules/net_vpc/psa_simple.tfvars b/tests/modules/net_vpc/psa_simple.tfvars deleted file mode 100644 index 51289fe04..000000000 --- a/tests/modules/net_vpc/psa_simple.tfvars +++ /dev/null @@ -1,7 +0,0 @@ -psa_config = { - ranges = { - bar = "172.16.100.0/24" - foo = "172.16.101.0/24" - } - routes = null -} diff --git a/tests/modules/net_vpc/psa_simple.yaml b/tests/modules/net_vpc/psa_simple.yaml deleted file mode 100644 index 019b443fa..000000000 --- a/tests/modules/net_vpc/psa_simple.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# 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. - -values: - google_compute_global_address.psa_ranges["bar"]: - address: 172.16.100.0 - address_type: INTERNAL - description: null - ip_version: null - name: bar - prefix_length: 24 - project: test-project - purpose: VPC_PEERING - google_compute_global_address.psa_ranges["foo"]: - address: 172.16.101.0 - address_type: INTERNAL - description: null - ip_version: null - name: foo - prefix_length: 24 - project: test-project - purpose: VPC_PEERING - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - enable_ula_internal_ipv6: null - name: test - project: test-project - routing_mode: GLOBAL - google_compute_network_peering_routes_config.psa_routes["1"]: - export_custom_routes: false - import_custom_routes: false - project: test-project - google_service_networking_connection.psa_connection["1"]: - reserved_peering_ranges: - - bar - - foo - service: servicenetworking.googleapis.com - -counts: - google_compute_global_address: 2 - google_compute_network: 1 - google_compute_network_peering_routes_config: 1 - google_service_networking_connection: 1 - -outputs: - bindings: {} - name: __missing__ - network: __missing__ - project_id: test-project - self_link: __missing__ - subnet_ips: {} - subnet_regions: {} - subnet_secondary_ranges: {} - subnet_self_links: {} - subnets: {} - subnets_proxy_only: {} - subnets_psc: {} diff --git a/tests/modules/net_vpc/simple.tfvars b/tests/modules/net_vpc/simple.tfvars deleted file mode 100644 index 6f848aa99..000000000 --- a/tests/modules/net_vpc/simple.tfvars +++ /dev/null @@ -1 +0,0 @@ -# skip boilerplate check diff --git a/tests/modules/net_vpc/simple.yaml b/tests/modules/net_vpc/simple.yaml deleted file mode 100644 index 004be7ecf..000000000 --- a/tests/modules/net_vpc/simple.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# 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. - -values: - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - name: test - project: test-project - routing_mode: GLOBAL - -counts: - google_compute_network: 1 - -outputs: - bindings: {} - project_id: test-project - subnet_ips: {} - subnet_regions: {} - subnet_secondary_ranges: {} - subnet_self_links: {} - subnets: {} - subnets_proxy_only: {} - subnets_psc: {} diff --git a/tests/modules/net_vpc/subnets.tfvars b/tests/modules/net_vpc/subnets.tfvars deleted file mode 100644 index 499e498f4..000000000 --- a/tests/modules/net_vpc/subnets.tfvars +++ /dev/null @@ -1,44 +0,0 @@ -subnet_iam = { - "europe-west1/a" = { - "roles/compute.networkUser" = [ - "user:a@example.com", "group:g-a@example.com" - ] - } - "europe-west1/c" = { - "roles/compute.networkUser" = [ - "user:c@example.com", "group:g-c@example.com" - ] - } -} -subnets = [ - { - name = "a" - region = "europe-west1" - ip_cidr_range = "10.0.0.0/24" - }, - { - name = "b" - region = "europe-west1" - ip_cidr_range = "10.0.1.0/24", - description = "Subnet b" - enable_private_access = false - }, - { - name = "c" - region = "europe-west1" - ip_cidr_range = "10.0.2.0/24" - secondary_ip_ranges = { - a = "192.168.0.0/24" - b = "192.168.1.0/24" - } - }, - { - name = "d" - region = "europe-west1" - ip_cidr_range = "10.0.3.0/24" - flow_logs_config = { - flow_sampling = 0.5 - aggregation_interval = "INTERVAL_10_MIN" - } - } -] diff --git a/tests/modules/net_vpc/subnets.yaml b/tests/modules/net_vpc/subnets.yaml deleted file mode 100644 index 9ccf31e60..000000000 --- a/tests/modules/net_vpc/subnets.yaml +++ /dev/null @@ -1,120 +0,0 @@ -# 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. - -values: - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - name: test - project: test-project - routing_mode: GLOBAL - google_compute_subnetwork.subnetwork["europe-west1/a"]: - description: Terraform-managed. - ip_cidr_range: 10.0.0.0/24 - log_config: [] - name: a - private_ip_google_access: true - project: test-project - region: europe-west1 - role: null - secondary_ip_range: [] - google_compute_subnetwork.subnetwork["europe-west1/b"]: - description: Subnet b - ip_cidr_range: 10.0.1.0/24 - log_config: [] - name: b - private_ip_google_access: false - project: test-project - region: europe-west1 - role: null - secondary_ip_range: [] - google_compute_subnetwork.subnetwork["europe-west1/c"]: - description: Terraform-managed. - ip_cidr_range: 10.0.2.0/24 - ipv6_access_type: null - log_config: [] - name: c - private_ip_google_access: true - project: test-project - region: europe-west1 - role: null - secondary_ip_range: - - ip_cidr_range: 192.168.0.0/24 - range_name: a - - ip_cidr_range: 192.168.1.0/24 - range_name: b - google_compute_subnetwork.subnetwork["europe-west1/d"]: - description: Terraform-managed. - ip_cidr_range: 10.0.3.0/24 - log_config: - - aggregation_interval: INTERVAL_10_MIN - filter_expr: 'true' - flow_sampling: 0.5 - metadata: INCLUDE_ALL_METADATA - metadata_fields: null - name: d - private_ip_google_access: true - project: test-project - region: europe-west1 - role: null - secondary_ip_range: [] - google_compute_subnetwork_iam_binding.binding["europe-west1/a.roles/compute.networkUser"]: - condition: [] - members: - - group:g-a@example.com - - user:a@example.com - project: test-project - region: europe-west1 - role: roles/compute.networkUser - subnetwork: a - google_compute_subnetwork_iam_binding.binding["europe-west1/c.roles/compute.networkUser"]: - condition: [] - members: - - group:g-c@example.com - - user:c@example.com - project: test-project - region: europe-west1 - role: roles/compute.networkUser - subnetwork: c - -counts: - google_compute_network: 1 - google_compute_subnetwork: 4 - google_compute_subnetwork_iam_binding: 2 - -outputs: - bindings: __missing__ - project_id: test-project - subnet_ips: - europe-west1/a: 10.0.0.0/24 - europe-west1/b: 10.0.1.0/24 - europe-west1/c: 10.0.2.0/24 - europe-west1/d: 10.0.3.0/24 - subnet_regions: - europe-west1/a: europe-west1 - europe-west1/b: europe-west1 - europe-west1/c: europe-west1 - europe-west1/d: europe-west1 - subnet_secondary_ranges: - europe-west1/a: {} - europe-west1/b: {} - europe-west1/c: - a: 192.168.0.0/24 - b: 192.168.1.0/24 - europe-west1/d: {} - subnet_self_links: __missing__ - subnets: __missing__ - subnets_proxy_only: {} - subnets_psc: {} diff --git a/tests/modules/net_vpc/tftest.yaml b/tests/modules/net_vpc/tftest.yaml index b2b09798b..c0dfa0adb 100644 --- a/tests/modules/net_vpc/tftest.yaml +++ b/tests/modules/net_vpc/tftest.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,12 +17,8 @@ common_tfvars: - common.tfvars tests: - simple: - subnets: - peering: shared_vpc: factory: - psa_simple: psa_routes_export: psa_routes_import: psa_routes_import_export: From a0cb67e1f4498d42198fa5a64c43c7d2c9fc5e5e Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 00:44:30 +0100 Subject: [PATCH 07/19] Add inventories to gcs examples --- modules/gcs/README.md | 37 ++++++--------- tests/modules/gcs/common.tfvars | 13 ------ .../gcs/{tftest.yaml => examples/cmek.yaml} | 17 ++++--- .../{prefix.yaml => examples/lifecycle.yaml} | 46 ++++++++----------- tests/modules/gcs/examples/notification.yaml | 31 +++++++++++++ .../retention-logging.yaml} | 22 ++++----- tests/modules/gcs/examples/simple.yaml | 46 +++++++++++++++++++ tests/modules/gcs/iam.tfvars | 3 -- tests/modules/gcs/prefix.tfvars | 1 - 9 files changed, 130 insertions(+), 86 deletions(-) delete mode 100644 tests/modules/gcs/common.tfvars rename tests/modules/gcs/{tftest.yaml => examples/cmek.yaml} (71%) rename tests/modules/gcs/{prefix.yaml => examples/lifecycle.yaml} (51%) create mode 100644 tests/modules/gcs/examples/notification.yaml rename tests/modules/gcs/{iam.yaml => examples/retention-logging.yaml} (65%) create mode 100644 tests/modules/gcs/examples/simple.yaml delete mode 100644 tests/modules/gcs/iam.tfvars delete mode 100644 tests/modules/gcs/prefix.tfvars diff --git a/modules/gcs/README.md b/modules/gcs/README.md index 439b4522d..07c5a6d7b 100644 --- a/modules/gcs/README.md +++ b/modules/gcs/README.md @@ -8,50 +8,46 @@ module "bucket" { project_id = "myproject" prefix = "test" name = "my-bucket" + versioning = true iam = { "roles/storage.admin" = ["group:storage@example.com"] } + labels = { + cost-center = "devops" + } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory=simple.yaml ``` ### Example with Cloud KMS ```hcl module "bucket" { - source = "./fabric/modules/gcs" - project_id = "myproject" - prefix = "test" - name = "my-bucket" - iam = { - "roles/storage.admin" = ["group:storage@example.com"] - } + source = "./fabric/modules/gcs" + project_id = "myproject" + name = "my-bucket" encryption_key = "my-encryption-key" } -# tftest modules=1 resources=2 +# tftest modules=1 resources=1 inventory=cmek.yaml ``` -### Example with retention policy +### Example with retention policy and logging ```hcl module "bucket" { source = "./fabric/modules/gcs" project_id = "myproject" - prefix = "test" name = "my-bucket" - iam = { - "roles/storage.admin" = ["group:storage@example.com"] - } retention_policy = { retention_period = 100 is_locked = true } logging_config = { - log_bucket = var.bucket + log_bucket = "log-bucket" log_object_prefix = null } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=1 inventory=retention-logging.yaml ``` ### Example with lifecycle rule @@ -60,11 +56,7 @@ module "bucket" { module "bucket" { source = "./fabric/modules/gcs" project_id = "myproject" - prefix = "test" name = "my-bucket" - iam = { - "roles/storage.admin" = ["group:storage@example.com"] - } lifecycle_rules = { lr-0 = { action = { @@ -77,7 +69,7 @@ module "bucket" { } } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=1 inventory=lifecycle.yaml ``` ### Minimal example with GCS notifications @@ -86,7 +78,6 @@ module "bucket" { module "bucket-gcs-notification" { source = "./fabric/modules/gcs" project_id = "myproject" - prefix = "test" name = "my-bucket" notification_config = { enabled = true @@ -97,7 +88,7 @@ module "bucket-gcs-notification" { custom_attributes = {} } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=4 inventory=notification.yaml ``` diff --git a/tests/modules/gcs/common.tfvars b/tests/modules/gcs/common.tfvars deleted file mode 100644 index 5bab53b25..000000000 --- a/tests/modules/gcs/common.tfvars +++ /dev/null @@ -1,13 +0,0 @@ -force_destroy = true -labels = { environment = "test" } -logging_config = { - log_bucket = "foo" -} -name = "test" -project_id = "test-project" -retention_policy = { - retention_period = 5 - is_locked = false -} -storage_class = "MULTI_REGIONAL" -versioning = true diff --git a/tests/modules/gcs/tftest.yaml b/tests/modules/gcs/examples/cmek.yaml similarity index 71% rename from tests/modules/gcs/tftest.yaml rename to tests/modules/gcs/examples/cmek.yaml index 22337d18d..ee92a5d22 100644 --- a/tests/modules/gcs/tftest.yaml +++ b/tests/modules/gcs/examples/cmek.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -module: modules/gcs -common_tfvars: - - common.tfvars -tests: - prefix: - iam: +values: + module.bucket.google_storage_bucket.bucket: + encryption: + - default_kms_key_name: my-encryption-key + name: my-bucket + project: myproject + +counts: + google_storage_bucket: 1 diff --git a/tests/modules/gcs/prefix.yaml b/tests/modules/gcs/examples/lifecycle.yaml similarity index 51% rename from tests/modules/gcs/prefix.yaml rename to tests/modules/gcs/examples/lifecycle.yaml index 6baee4a15..69eeea41f 100644 --- a/tests/modules/gcs/prefix.yaml +++ b/tests/modules/gcs/examples/lifecycle.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,32 +13,26 @@ # limitations under the License. values: - google_storage_bucket.bucket: - force_destroy: true - labels: - environment: test - location: EU - logging: - - log_bucket: foo - name: foo-test - project: test-project - retention_policy: - - is_locked: false - retention_period: 5 - storage_class: MULTI_REGIONAL - uniform_bucket_level_access: true - versioning: - - enabled: true + module.bucket.google_storage_bucket.bucket: + lifecycle_rule: + - action: + - storage_class: STANDARD + type: SetStorageClass + condition: + - age: 30 + created_before: '' + custom_time_before: '' + days_since_custom_time: null + days_since_noncurrent_time: null + matches_prefix: [] + matches_storage_class: [] + matches_suffix: [] + noncurrent_time_before: '' + num_newer_versions: null + name: my-bucket + project: myproject counts: google_storage_bucket: 1 - modules: 0 - resources: 1 -outputs: - bucket: __missing__ - id: foo-test - name: foo-test - notification: null - topic: null - url: __missing__ +outputs: {} diff --git a/tests/modules/gcs/examples/notification.yaml b/tests/modules/gcs/examples/notification.yaml new file mode 100644 index 000000000..9536e89b4 --- /dev/null +++ b/tests/modules/gcs/examples/notification.yaml @@ -0,0 +1,31 @@ +# Copyright 2023 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.bucket-gcs-notification.google_pubsub_topic.topic[0]: {} + module.bucket-gcs-notification.google_pubsub_topic_iam_binding.binding[0]: {} + module.bucket-gcs-notification.google_storage_bucket.bucket: + name: my-bucket + project: myproject + module.bucket-gcs-notification.google_storage_notification.notification[0]: + bucket: my-bucket + event_types: + - OBJECT_FINALIZE + payload_format: JSON_API_V1 + +counts: + google_pubsub_topic: 1 + google_pubsub_topic_iam_binding: 1 + google_storage_bucket: 1 + google_storage_notification: 1 diff --git a/tests/modules/gcs/iam.yaml b/tests/modules/gcs/examples/retention-logging.yaml similarity index 65% rename from tests/modules/gcs/iam.yaml rename to tests/modules/gcs/examples/retention-logging.yaml index 8a85a4bd6..962414207 100644 --- a/tests/modules/gcs/iam.yaml +++ b/tests/modules/gcs/examples/retention-logging.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,18 +13,14 @@ # limitations under the License. values: - google_storage_bucket.bucket: - name: test - - google_storage_bucket_iam_binding.bindings["roles/storage.admin"]: - bucket: test - condition: [] - members: - - user:a@example.org - role: roles/storage.admin + module.bucket.google_storage_bucket.bucket: + logging: + - log_bucket: log-bucket + name: my-bucket + project: myproject + retention_policy: + - is_locked: true + retention_period: 100 counts: google_storage_bucket: 1 - google_storage_bucket_iam_binding: 1 - modules: 0 - resources: 2 diff --git a/tests/modules/gcs/examples/simple.yaml b/tests/modules/gcs/examples/simple.yaml new file mode 100644 index 000000000..bc2630b87 --- /dev/null +++ b/tests/modules/gcs/examples/simple.yaml @@ -0,0 +1,46 @@ +# Copyright 2023 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.bucket.google_storage_bucket.bucket: + autoclass: [] + cors: [] + custom_placement_config: [] + default_event_based_hold: null + encryption: [] + force_destroy: false + labels: + cost-center: devops + lifecycle_rule: [] + location: EU + logging: [] + name: test-my-bucket + project: myproject + requester_pays: null + retention_policy: [] + storage_class: MULTI_REGIONAL + timeouts: null + uniform_bucket_level_access: true + versioning: + - enabled: true + module.bucket.google_storage_bucket_iam_binding.bindings["roles/storage.admin"]: + bucket: test-my-bucket + condition: [] + members: + - group:storage@example.com + role: roles/storage.admin + +counts: + google_storage_bucket: 1 + google_storage_bucket_iam_binding: 1 diff --git a/tests/modules/gcs/iam.tfvars b/tests/modules/gcs/iam.tfvars deleted file mode 100644 index cfb3a0148..000000000 --- a/tests/modules/gcs/iam.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -iam = { - "roles/storage.admin" = ["user:a@example.org"] -} diff --git a/tests/modules/gcs/prefix.tfvars b/tests/modules/gcs/prefix.tfvars deleted file mode 100644 index 0031d561d..000000000 --- a/tests/modules/gcs/prefix.tfvars +++ /dev/null @@ -1 +0,0 @@ -prefix = "foo" From 2aad7845a4f868bdee1b5d55a2f82e5f74e04515 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 11:20:00 +0100 Subject: [PATCH 08/19] Allow dashes and underscores in tftest file ids --- tests/examples/conftest.py | 4 ++-- tests/examples/test_plan.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 16863e26d..4d3d85ee6 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import marko FABRIC_ROOT = Path(__file__).parents[2] -FILE_TEST_RE = re.compile(r'# tftest-file +id=(\w+) +path=([\S]+)') +FILE_TEST_RE = re.compile(r'# tftest-file +id=([\w_.-]+) +path=([\S]+)') Example = collections.namedtuple('Example', 'name code module files') File = collections.namedtuple('File', 'path content') diff --git a/tests/examples/test_plan.py b/tests/examples/test_plan.py index 2dd42a7ad..261276f73 100644 --- a/tests/examples/test_plan.py +++ b/tests/examples/test_plan.py @@ -18,7 +18,7 @@ from pathlib import Path BASE_PATH = Path(__file__).parent COUNT_TEST_RE = re.compile(r'# tftest +modules=(\d+) +resources=(\d+)' + - r'(?: +files=([\w,-.]+))?' + + r'(?: +files=([\w,_-]+))?' + r'(?: +inventory=([\w\-.]+))?') @@ -49,10 +49,10 @@ def test_example(plan_validator, tmp_path, example): summary = plan_validator(module_path=tmp_path, inventory_paths=inventory, tf_var_files=[]) - # import yaml - # print(yaml.dump({"values": summary.values})) - # print(yaml.dump({"counts": summary.counts})) - # print(yaml.dump({"outputs": summary.outputs})) + import yaml + print(yaml.dump({"values": summary.values})) + print(yaml.dump({"counts": summary.counts})) + print(yaml.dump({"outputs": summary.outputs})) counts = summary.counts num_modules, num_resources = counts['modules'], counts['resources'] From a12089ef8cc68b9e0dce58114c3befafe019e942 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 11:20:38 +0100 Subject: [PATCH 09/19] Move VPC factory and route tests to examples. --- modules/net-vpc/README.md | 49 +++++- .../modules/net_vpc/data/factory-subnet.yaml | 23 --- .../modules/net_vpc/data/factory-subnet2.yaml | 17 -- tests/modules/net_vpc/examples/factory.yaml | 50 ++++++ tests/modules/net_vpc/examples/routes.yaml | 146 ++++++++++++++++++ tests/modules/net_vpc/factory.tfvars | 1 - tests/modules/net_vpc/factory.yaml | 44 ------ tests/modules/net_vpc/test_routes.py | 47 ------ tests/modules/net_vpc/tftest.yaml | 1 - 9 files changed, 243 insertions(+), 135 deletions(-) delete mode 100644 tests/modules/net_vpc/data/factory-subnet.yaml delete mode 100644 tests/modules/net_vpc/data/factory-subnet2.yaml create mode 100644 tests/modules/net_vpc/examples/factory.yaml create mode 100644 tests/modules/net_vpc/examples/routes.yaml delete mode 100644 tests/modules/net_vpc/factory.tfvars delete mode 100644 tests/modules/net_vpc/factory.yaml delete mode 100644 tests/modules/net_vpc/test_routes.py diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 5a9013780..002c738d8 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -314,11 +314,17 @@ module "vpc" { name = "my-network" data_folder = "config/subnets" } -# tftest modules=1 resources=2 files=subnets +# tftest modules=1 resources=3 files=subnet-simple,subnet-detailed inventory=factory.yaml ``` ```yaml -# tftest-file id=subnets path=config/subnets/subnet-name.yaml +# tftest-file id=subnet-simple path=config/subnets/subnet-simple.yaml +region: europe-west4 +ip_cidr_range: 10.0.1.0/24 +``` + +```yaml +# tftest-file id=subnet-detailed path=config/subnets/subnet-detailed.yaml region: europe-west1 description: Sample description ip_cidr_range: 10.0.0.0/24 @@ -337,6 +343,45 @@ flow_logs: # enable, set to empty map to use defaults ``` +### Custom Routes + +VPC routes can be configured through the `routes` variable. + +```hcl +locals { + route_types = { + gateway = "global/gateways/default-internet-gateway" + instance = "zones/europe-west1-b/test" + ip = "192.168.0.128" + ilb = "regions/europe-west1/forwardingRules/test" + vpn_tunnel = "regions/europe-west1/vpnTunnels/foo" + } +} +module "vpc" { + source = "./fabric/modules/net-vpc" + for_each = local.route_types + project_id = "my-project" + name = "my-network-with-route-${replace(each.key, "_", "-")}" + routes = { + next-hop = { + dest_range = "192.168.128.0/24" + tags = null + next_hop_type = each.key + next_hop = each.value + } + gateway = { + dest_range = "0.0.0.0/0", + priority = 100 + tags = ["tag-a"] + next_hop_type = "gateway", + next_hop = "global/gateways/default-internet-gateway" + } + } +} +# tftest modules=5 resources=15 inventory=routes.yaml +``` + + ## Variables | name | description | type | required | default | diff --git a/tests/modules/net_vpc/data/factory-subnet.yaml b/tests/modules/net_vpc/data/factory-subnet.yaml deleted file mode 100644 index d0f4bd8f1..000000000 --- a/tests/modules/net_vpc/data/factory-subnet.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# 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. - -region: europe-west1 -description: Sample description -ip_cidr_range: 10.128.0.0/24 -enable_private_access: false -iam_users: ["foobar@example.com"] -iam_groups: ["lorem@example.com"] -iam_service_accounts: ["foobar@project-id.iam.gserviceaccount.com"] -secondary_ip_ranges: - secondary-range-a: 192.168.128.0/24 diff --git a/tests/modules/net_vpc/data/factory-subnet2.yaml b/tests/modules/net_vpc/data/factory-subnet2.yaml deleted file mode 100644 index e110c1625..000000000 --- a/tests/modules/net_vpc/data/factory-subnet2.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# 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. - -region: europe-west4 -description: Sample description -ip_cidr_range: 10.129.0.0/24 diff --git a/tests/modules/net_vpc/examples/factory.yaml b/tests/modules/net_vpc/examples/factory.yaml new file mode 100644 index 000000000..48671c292 --- /dev/null +++ b/tests/modules/net_vpc/examples/factory.yaml @@ -0,0 +1,50 @@ +# Copyright 2023 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.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + routing_mode: GLOBAL + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/subnet-detailed"]: + description: Sample description + ip_cidr_range: 10.0.0.0/24 + log_config: + - aggregation_interval: INTERVAL_5_SEC + filter_expr: 'true' + flow_sampling: 0.5 + metadata: INCLUDE_ALL_METADATA + metadata_fields: null + name: subnet-detailed + private_ip_google_access: false + project: my-project + region: europe-west1 + role: null + secondary_ip_range: + - ip_cidr_range: 192.168.0.0/24 + range_name: secondary-range-a + module.vpc.google_compute_subnetwork.subnetwork["europe-west4/subnet-simple"]: + description: Terraform-managed. + ip_cidr_range: 10.0.1.0/24 + log_config: [] + name: subnet-simple + private_ip_google_access: true + project: my-project + region: europe-west4 + role: null + secondary_ip_range: [] + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/examples/routes.yaml b/tests/modules/net_vpc/examples/routes.yaml new file mode 100644 index 000000000..205197c82 --- /dev/null +++ b/tests/modules/net_vpc/examples/routes.yaml @@ -0,0 +1,146 @@ +# Copyright 2023 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.vpc["gateway"].google_compute_network.network[0]: + name: my-network-with-route-gateway + project: my-project + routing_mode: GLOBAL + module.vpc["gateway"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-gateway-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["gateway"].google_compute_route.gateway["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-gateway-next-hop + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["ilb"].google_compute_network.network[0]: + name: my-network-with-route-ilb + project: my-project + routing_mode: GLOBAL + module.vpc["ilb"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-ilb-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["ilb"].google_compute_route.ilb["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-ilb-next-hop + next_hop_gateway: null + next_hop_ilb: regions/europe-west1/forwardingRules/test + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["instance"].google_compute_network.network[0]: + name: my-network-with-route-instance + project: my-project + routing_mode: GLOBAL + module.vpc["instance"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-instance-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["instance"].google_compute_route.instance["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-instance-next-hop + next_hop_gateway: null + next_hop_ilb: null + next_hop_instance: zones/europe-west1-b/test + next_hop_instance_zone: europe-west1-b + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["ip"].google_compute_network.network[0]: + name: my-network-with-route-ip + project: my-project + routing_mode: GLOBAL + module.vpc["ip"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-ip-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["ip"].google_compute_route.ip["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-ip-next-hop + next_hop_gateway: null + next_hop_ilb: null + next_hop_instance: null + next_hop_ip: 192.168.0.128 + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["vpn_tunnel"].google_compute_network.network[0]: + name: my-network-with-route-vpn-tunnel + project: my-project + routing_mode: GLOBAL + module.vpc["vpn_tunnel"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-vpn-tunnel-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["vpn_tunnel"].google_compute_route.vpn_tunnel["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-vpn-tunnel-next-hop + next_hop_gateway: null + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: regions/europe-west1/vpnTunnels/foo + priority: 1000 + project: my-project + tags: null + +counts: + google_compute_network: 5 + google_compute_route: 10 diff --git a/tests/modules/net_vpc/factory.tfvars b/tests/modules/net_vpc/factory.tfvars deleted file mode 100644 index 8c4d4a28c..000000000 --- a/tests/modules/net_vpc/factory.tfvars +++ /dev/null @@ -1 +0,0 @@ -data_folder = "../../tests/modules/net_vpc/data" diff --git a/tests/modules/net_vpc/factory.yaml b/tests/modules/net_vpc/factory.yaml deleted file mode 100644 index 9cf628d09..000000000 --- a/tests/modules/net_vpc/factory.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# 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. - -values: - google_compute_subnetwork.subnetwork["europe-west1/factory-subnet"]: - description: 'Sample description' - ip_cidr_range: '10.128.0.0/24' - ipv6_access_type: null - log_config: [] - name: 'factory-subnet' - private_ip_google_access: false - project: 'test-project' - region: 'europe-west1' - role: null - secondary_ip_range: - - ip_cidr_range: '192.168.128.0/24' - range_name: 'secondary-range-a' - google_compute_subnetwork.subnetwork["europe-west4/factory-subnet2"]: - description: 'Sample description' - ip_cidr_range: '10.129.0.0/24' - log_config: [] - name: 'factory-subnet2' - private_ip_google_access: true - project: 'test-project' - region: 'europe-west4' - role: null - secondary_ip_range: [] - - # FIXME: should we have some bindings here? - -counts: - google_compute_network: 1 - google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/test_routes.py b/tests/modules/net_vpc/test_routes.py deleted file mode 100644 index 01d9673dd..000000000 --- a/tests/modules/net_vpc/test_routes.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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. - -import pytest - -_route_parameters = [('gateway', 'global/gateways/default-internet-gateway'), - ('instance', 'zones/europe-west1-b/test'), - ('ip', '192.168.0.128'), - ('ilb', 'regions/europe-west1/forwardingRules/test'), - ('vpn_tunnel', 'regions/europe-west1/vpnTunnels/foo')] - - -@pytest.mark.parametrize('next_hop_type,next_hop', _route_parameters) -def test_vpc_routes(plan_summary, next_hop_type, next_hop): - 'Test vpc routes.' - - var_routes = '''{ - next-hop = { - dest_range = "192.168.128.0/24" - tags = null - next_hop_type = "%s" - next_hop = "%s" - } - gateway = { - dest_range = "0.0.0.0/0", - priority = 100 - tags = ["tag-a"] - next_hop_type = "gateway", - next_hop = "global/gateways/default-internet-gateway" - } - }''' % (next_hop_type, next_hop) - summary = plan_summary('modules/net-vpc', tf_var_files=['common.tfvars'], - routes=var_routes) - assert len(summary.values) == 3 - route = summary.values[f'google_compute_route.{next_hop_type}["next-hop"]'] - assert route[f'next_hop_{next_hop_type}'] == next_hop diff --git a/tests/modules/net_vpc/tftest.yaml b/tests/modules/net_vpc/tftest.yaml index c0dfa0adb..5e9668ea4 100644 --- a/tests/modules/net_vpc/tftest.yaml +++ b/tests/modules/net_vpc/tftest.yaml @@ -18,7 +18,6 @@ common_tfvars: tests: shared_vpc: - factory: psa_routes_export: psa_routes_import: psa_routes_import_export: From 1e0d7776e18a7f11497161a4a68d9fdd9bbdd12e Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 12:21:48 +0100 Subject: [PATCH 10/19] Update DNS tests --- modules/dns/README.md | 32 +++- modules/dns/main.tf | 2 +- .../modules/dns/examples/forwarding-zone.yaml | 34 +++++ tests/modules/dns/examples/peering-zone.yaml | 34 +++++ tests/modules/dns/examples/private-zone.yaml | 50 +++++++ tests/modules/dns/examples/public-zone.yaml | 38 +++++ tests/modules/dns/examples/reverse-zone.yaml | 27 ++++ .../dns/examples/routing-policies.yaml | 80 ++++++++++ tests/modules/dns/fixture/main.tf | 27 ---- tests/modules/dns/fixture/variables.tf | 62 -------- tests/modules/dns/no_clients.tfvars | 5 + tests/modules/dns/no_clients.yaml | 25 ++++ tests/modules/dns/null_forwarders.tfvars | 4 + tests/modules/dns/null_forwarders.yaml | 20 +++ tests/modules/dns/test_plan.py | 138 ------------------ tests/modules/dns/tftest.yaml | 19 +++ 16 files changed, 363 insertions(+), 234 deletions(-) create mode 100644 tests/modules/dns/examples/forwarding-zone.yaml create mode 100644 tests/modules/dns/examples/peering-zone.yaml create mode 100644 tests/modules/dns/examples/private-zone.yaml create mode 100644 tests/modules/dns/examples/public-zone.yaml create mode 100644 tests/modules/dns/examples/reverse-zone.yaml create mode 100644 tests/modules/dns/examples/routing-policies.yaml delete mode 100644 tests/modules/dns/fixture/main.tf delete mode 100644 tests/modules/dns/fixture/variables.tf create mode 100644 tests/modules/dns/no_clients.tfvars create mode 100644 tests/modules/dns/no_clients.yaml create mode 100644 tests/modules/dns/null_forwarders.tfvars create mode 100644 tests/modules/dns/null_forwarders.yaml delete mode 100644 tests/modules/dns/test_plan.py create mode 100644 tests/modules/dns/tftest.yaml diff --git a/modules/dns/README.md b/modules/dns/README.md index 9e461f0e5..4803a13ce 100644 --- a/modules/dns/README.md +++ b/modules/dns/README.md @@ -21,7 +21,7 @@ module "private-dns" { "A myhost" = { ttl = 600, records = ["10.0.0.120"] } } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=private-zone.yaml ``` ### Forwarding Zone @@ -36,7 +36,7 @@ module "private-dns" { client_networks = [var.vpc.self_link] forwarders = { "10.0.1.1" = null, "1.2.3.4" = "private" } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=forwarding-zone.yaml ``` ### Peering Zone @@ -47,11 +47,12 @@ module "private-dns" { project_id = "myproject" type = "peering" name = "test-example" - domain = "test.example." + domain = "." + description = "Forwarding zone for ." client_networks = [var.vpc.self_link] peer_network = var.vpc2.self_link } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=peering-zone.yaml ``` ### Routing Policies @@ -84,7 +85,7 @@ module "private-dns" { } } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=4 inventory=routing-policies.yaml ``` ### Reverse Lookup Zone @@ -98,10 +99,29 @@ module "private-dns" { domain = "0.0.10.in-addr.arpa." client_networks = [var.vpc.self_link] } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=reverse-zone.yaml ``` +### Public Zone + +```hcl +module "public-dns" { + source = "./fabric/modules/dns" + project_id = "myproject" + type = "public" + name = "example" + domain = "example.com." + recordsets = { + "A myhost" = { ttl = 300, records = ["127.0.0.1"] } + } +} +# tftest modules=1 resources=3 inventory=public-zone.yaml +``` + + + + ## Variables | name | description | type | required | default | diff --git a/modules/dns/main.tf b/modules/dns/main.tf index ca30c7d0c..edf342ef6 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/modules/dns/examples/forwarding-zone.yaml b/tests/modules/dns/examples/forwarding-zone.yaml new file mode 100644 index 000000000..4a09114ee --- /dev/null +++ b/tests/modules/dns/examples/forwarding-zone.yaml @@ -0,0 +1,34 @@ +# Copyright 2023 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.private-dns.google_dns_managed_zone.non-public[0]: + dns_name: test.example. + forwarding_config: + - target_name_servers: + - forwarding_path: '' + ipv4_address: 10.0.1.1 + - forwarding_path: private + ipv4_address: 1.2.3.4 + name: test-example + private_visibility_config: + - gke_clusters: [] + networks: + - network_url: projects/xxx/global/networks/aaa + project: myproject + visibility: private + +counts: + google_dns_managed_zone: 1 + diff --git a/tests/modules/dns/examples/peering-zone.yaml b/tests/modules/dns/examples/peering-zone.yaml new file mode 100644 index 000000000..9f16adab6 --- /dev/null +++ b/tests/modules/dns/examples/peering-zone.yaml @@ -0,0 +1,34 @@ +# Copyright 2023 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.private-dns.google_dns_managed_zone.non-public[0]: + description: Forwarding zone for . + dns_name: . + forwarding_config: [] + name: test-example + peering_config: + - target_network: + - network_url: projects/xxx/global/networks/ccc + private_visibility_config: + - gke_clusters: [] + networks: + - network_url: projects/xxx/global/networks/aaa + project: myproject + visibility: private + +counts: + google_dns_managed_zone: 1 + +outputs: {} diff --git a/tests/modules/dns/examples/private-zone.yaml b/tests/modules/dns/examples/private-zone.yaml new file mode 100644 index 000000000..f64266450 --- /dev/null +++ b/tests/modules/dns/examples/private-zone.yaml @@ -0,0 +1,50 @@ +# Copyright 2023 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.private-dns.google_dns_managed_zone.non-public[0]: + description: Terraform managed. + dns_name: test.example. + force_destroy: false + forwarding_config: [] + name: test-example + peering_config: [] + private_visibility_config: + - gke_clusters: [] + networks: + - network_url: projects/xxx/global/networks/aaa + project: myproject + visibility: private + module.private-dns.google_dns_record_set.cloud-static-records["A localhost"]: + managed_zone: test-example + name: localhost.test.example. + project: myproject + routing_policy: [] + rrdatas: + - 127.0.0.1 + ttl: 300 + type: A + module.private-dns.google_dns_record_set.cloud-static-records["A myhost"]: + managed_zone: test-example + name: myhost.test.example. + project: myproject + routing_policy: [] + rrdatas: + - 10.0.0.120 + ttl: 600 + type: A + +counts: + google_dns_managed_zone: 1 + google_dns_record_set: 2 diff --git a/tests/modules/dns/examples/public-zone.yaml b/tests/modules/dns/examples/public-zone.yaml new file mode 100644 index 000000000..0f8067a76 --- /dev/null +++ b/tests/modules/dns/examples/public-zone.yaml @@ -0,0 +1,38 @@ +# Copyright 2023 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.public-dns.google_dns_managed_zone.public[0]: + dns_name: example.com. + name: example + project: myproject + visibility: public + module.public-dns.google_dns_record_set.cloud-static-records["A myhost"]: + managed_zone: example + name: myhost.example.com. + project: myproject + routing_policy: [] + rrdatas: + - 127.0.0.1 + ttl: 300 + type: A + +counts: + google_dns_keys: 1 + google_dns_managed_zone: 1 + google_dns_record_set: 1 + modules: 1 + resources: 3 + +outputs: {} diff --git a/tests/modules/dns/examples/reverse-zone.yaml b/tests/modules/dns/examples/reverse-zone.yaml new file mode 100644 index 000000000..17e76a12c --- /dev/null +++ b/tests/modules/dns/examples/reverse-zone.yaml @@ -0,0 +1,27 @@ +# Copyright 2023 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.private-dns.google_dns_managed_zone.non-public[0]: + description: Terraform managed. + dns_name: 0.0.10.in-addr.arpa. + name: test-example + project: myproject + reverse_lookup: true + visibility: private + +counts: + google_dns_managed_zone: 1 + +outputs: {} diff --git a/tests/modules/dns/examples/routing-policies.yaml b/tests/modules/dns/examples/routing-policies.yaml new file mode 100644 index 000000000..45b19276c --- /dev/null +++ b/tests/modules/dns/examples/routing-policies.yaml @@ -0,0 +1,80 @@ +# Copyright 2023 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.private-dns.google_dns_managed_zone.non-public[0]: + dns_name: test.example. + name: test-example + project: myproject + module.private-dns.google_dns_record_set.cloud-geo-records["A geo"]: + managed_zone: test-example + name: geo.test.example. + project: myproject + routing_policy: + - enable_geo_fencing: null + geo: + - health_checked_targets: [] + location: europe-west1 + rrdatas: + - 10.0.0.1 + - health_checked_targets: [] + location: europe-west2 + rrdatas: + - 10.0.0.2 + - health_checked_targets: [] + location: europe-west3 + rrdatas: + - 10.0.0.3 + primary_backup: [] + wrr: [] + rrdatas: null + ttl: 300 + type: A + module.private-dns.google_dns_record_set.cloud-static-records["A regular"]: + managed_zone: test-example + name: regular.test.example. + project: myproject + routing_policy: [] + rrdatas: + - 10.20.0.1 + ttl: 300 + type: A + module.private-dns.google_dns_record_set.cloud-wrr-records["A wrr"]: + managed_zone: test-example + name: wrr.test.example. + project: myproject + routing_policy: + - enable_geo_fencing: null + geo: [] + primary_backup: [] + wrr: + - health_checked_targets: [] + rrdatas: + - 10.10.0.1 + weight: 0.6 + - health_checked_targets: [] + rrdatas: + - 10.10.0.2 + weight: 0.2 + - health_checked_targets: [] + rrdatas: + - 10.10.0.3 + weight: 0.2 + rrdatas: null + ttl: 600 + type: A + +counts: + google_dns_managed_zone: 1 + google_dns_record_set: 3 diff --git a/tests/modules/dns/fixture/main.tf b/tests/modules/dns/fixture/main.tf deleted file mode 100644 index bab319204..000000000 --- a/tests/modules/dns/fixture/main.tf +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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/dns" - project_id = "my-project" - name = "test" - domain = "test.example." - client_networks = var.client_networks - type = var.type - forwarders = var.forwarders - peer_network = var.peer_network - recordsets = var.recordsets -} diff --git a/tests/modules/dns/fixture/variables.tf b/tests/modules/dns/fixture/variables.tf deleted file mode 100644 index 8e55a287a..000000000 --- a/tests/modules/dns/fixture/variables.tf +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 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 "client_networks" { - type = list(string) - default = [ - "https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default" - ] -} - -variable "forwarders" { - type = map(string) - default = {} -} - -variable "peer_network" { - type = string - default = null -} - -variable "recordsets" { - type = any - default = { - "A localhost" = { ttl = 300, records = ["127.0.0.1"] } - "A local-host.test.example." = { ttl = 300, records = ["127.0.0.2"] } - "CNAME *" = { ttl = 300, records = ["localhost.example.org."] } - "A " = { ttl = 300, records = ["127.0.0.3"] } - "A geo" = { - geo_routing = [ - { location = "europe-west1", records = ["127.0.0.4"] }, - { location = "europe-west2", records = ["127.0.0.5"] }, - { location = "europe-west3", records = ["127.0.0.6"] } - ] - } - "A wrr" = { - ttl = 600 - wrr_routing = [ - { weight = 0.6, records = ["127.0.0.7"] }, - { weight = 0.2, records = ["127.0.0.8"] }, - { weight = 0.2, records = ["127.0.0.9"] } - ] - } - } -} - -variable "type" { - type = string - default = "private" -} diff --git a/tests/modules/dns/no_clients.tfvars b/tests/modules/dns/no_clients.tfvars new file mode 100644 index 000000000..97b722734 --- /dev/null +++ b/tests/modules/dns/no_clients.tfvars @@ -0,0 +1,5 @@ +type = "private" +domain = "test.example." +name = "test" +project_id = "my-project" +client_networks = [] diff --git a/tests/modules/dns/no_clients.yaml b/tests/modules/dns/no_clients.yaml new file mode 100644 index 000000000..42f628c9c --- /dev/null +++ b/tests/modules/dns/no_clients.yaml @@ -0,0 +1,25 @@ +# Copyright 2023 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: + google_dns_managed_zone.non-public[0]: + dns_name: test.example. + name: test + private_visibility_config: [] + visibility: private + +counts: + google_dns_managed_zone: 1 + modules: 0 + resources: 1 diff --git a/tests/modules/dns/null_forwarders.tfvars b/tests/modules/dns/null_forwarders.tfvars new file mode 100644 index 000000000..4514d6395 --- /dev/null +++ b/tests/modules/dns/null_forwarders.tfvars @@ -0,0 +1,4 @@ +type = "forwarding" +domain = "test.example." +name = "test" +project_id = "my-project" diff --git a/tests/modules/dns/null_forwarders.yaml b/tests/modules/dns/null_forwarders.yaml new file mode 100644 index 000000000..bbe637fc2 --- /dev/null +++ b/tests/modules/dns/null_forwarders.yaml @@ -0,0 +1,20 @@ +# Copyright 2023 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: + google_dns_managed_zone.non-public[0]: + forwarding_config: [] + +counts: + google_dns_managed_zone: 1 diff --git a/tests/modules/dns/test_plan.py b/tests/modules/dns/test_plan.py deleted file mode 100644 index 5cc1ba709..000000000 --- a/tests/modules/dns/test_plan.py +++ /dev/null @@ -1,138 +0,0 @@ -# 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. - - -def test_private(plan_runner): - "Test private zone with three recordsets." - _, resources = plan_runner() - assert len(resources) == 7 - assert set(r['type'] for r in resources) == { - 'google_dns_record_set', 'google_dns_managed_zone' - } - for r in resources: - if r['type'] != 'google_dns_managed_zone': - continue - assert r['values']['visibility'] == 'private' - assert len(r['values']['private_visibility_config']) == 1 - - -def test_private_recordsets(plan_runner): - "Test recordsets in private zone." - _, resources = plan_runner() - recordsets = [ - r['values'] for r in resources if r['type'] == 'google_dns_record_set' - ] - - assert set(r['name'] for r in recordsets) == { - 'localhost.test.example.', 'local-host.test.example.', '*.test.example.', - "test.example.", "geo.test.example.", "wrr.test.example." - } - - for r in recordsets: - if r['name'] not in ['wrr.test.example.', 'geo.test.example.']: - assert r['routing_policy'] == [] - assert r['rrdatas'] != [] - - -def test_routing_policies(plan_runner): - "Test recordsets with routing policies." - _, resources = plan_runner() - recordsets = [ - r['values'] for r in resources if r['type'] == 'google_dns_record_set' - ] - geo_zone = [ - r['values'] for r in resources if r['address'] == - 'module.test.google_dns_record_set.cloud-geo-records["A geo"]' - ][0] - assert geo_zone['name'] == 'geo.test.example.' - assert geo_zone['routing_policy'][0]['wrr'] == [] - geo_policy = geo_zone['routing_policy'][0]['geo'] - assert geo_policy[0]['location'] == 'europe-west1' - assert geo_policy[0]['rrdatas'] == ['127.0.0.4'] - assert geo_policy[1]['location'] == 'europe-west2' - assert geo_policy[1]['rrdatas'] == ['127.0.0.5'] - assert geo_policy[2]['location'] == 'europe-west3' - assert geo_policy[2]['rrdatas'] == ['127.0.0.6'] - - wrr_zone = [ - r['values'] for r in resources if r['address'] == - 'module.test.google_dns_record_set.cloud-wrr-records["A wrr"]' - ][0] - assert wrr_zone['name'] == 'wrr.test.example.' - wrr_policy = wrr_zone['routing_policy'][0]['wrr'] - assert wrr_policy[0]['weight'] == 0.6 - assert wrr_policy[0]['rrdatas'] == ['127.0.0.7'] - assert wrr_policy[1]['weight'] == 0.2 - assert wrr_policy[1]['rrdatas'] == ['127.0.0.8'] - assert wrr_policy[2]['weight'] == 0.2 - assert wrr_policy[2]['rrdatas'] == ['127.0.0.9'] - assert wrr_zone['routing_policy'][0]['geo'] == [] - - -def test_private_no_networks(plan_runner): - "Test private zone not exposed to any network." - _, resources = plan_runner(client_networks='[]') - for r in resources: - if r['type'] != 'google_dns_managed_zone': - continue - assert r['values']['visibility'] == 'private' - assert len(r['values']['private_visibility_config']) == 0 - - -def test_forwarding_recordsets_null_forwarders(plan_runner): - "Test forwarding zone with wrong set of attributes does not break." - _, resources = plan_runner(type='forwarding') - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['forwarding_config'] == [] - - -def test_forwarding(plan_runner): - "Test forwarding zone with single forwarder." - _, resources = plan_runner(type='forwarding', recordsets='null', - forwarders='{ "1.2.3.4" = null }') - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['forwarding_config'] == [{ - 'target_name_servers': [{ - 'forwarding_path': '', - 'ipv4_address': '1.2.3.4' - }] - }] - - -def test_peering(plan_runner): - "Test peering zone." - _, resources = plan_runner(type='peering', recordsets='null', - peer_network='dummy-vpc-self-link') - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['peering_config'] == [{ - 'target_network': [{ - 'network_url': 'dummy-vpc-self-link' - }] - }] - - -def test_public(plan_runner): - "Test public zone with two recordsets." - _, resources = plan_runner(type='public') - for r in resources: - if r['type'] != 'google_dns_managed_zone': - continue - assert r['values']['visibility'] == 'public' - assert r['values']['private_visibility_config'] == [] diff --git a/tests/modules/dns/tftest.yaml b/tests/modules/dns/tftest.yaml new file mode 100644 index 000000000..5172a013b --- /dev/null +++ b/tests/modules/dns/tftest.yaml @@ -0,0 +1,19 @@ +# Copyright 2023 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: modules/dns + +tests: + no_clients: + null_forwarders: From 9c9aafb3f1a23e224c1ca85bf2375abad689ff67 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 13:03:30 +0100 Subject: [PATCH 11/19] Update gke-cluster tests --- modules/gke-cluster/README.md | 34 ++++++++++++-- .../gke_cluster/examples/autopilot.yaml | 32 +++++++++++++ tests/modules/gke_cluster/examples/basic.yaml | 42 +++++++++++++++++ .../gke_cluster/examples/dataplane-v2.yaml | 45 +++++++++++++++++++ tests/modules/gke_cluster/fixture/main.tf | 29 ------------ .../modules/gke_cluster/fixture/variables.tf | 43 ------------------ tests/modules/gke_cluster/test_plan.py | 38 ---------------- 7 files changed, 150 insertions(+), 113 deletions(-) create mode 100644 tests/modules/gke_cluster/examples/autopilot.yaml create mode 100644 tests/modules/gke_cluster/examples/basic.yaml create mode 100644 tests/modules/gke_cluster/examples/dataplane-v2.yaml delete mode 100644 tests/modules/gke_cluster/fixture/main.tf delete mode 100644 tests/modules/gke_cluster/fixture/variables.tf delete mode 100644 tests/modules/gke_cluster/test_plan.py diff --git a/modules/gke-cluster/README.md b/modules/gke-cluster/README.md index caf1fec93..0ba75cd61 100644 --- a/modules/gke-cluster/README.md +++ b/modules/gke-cluster/README.md @@ -33,7 +33,7 @@ module "cluster-1" { environment = "dev" } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=basic.yaml ``` ### GKE Cluster with Dataplane V2 enabled @@ -42,7 +42,7 @@ module "cluster-1" { module "cluster-1" { source = "./fabric/modules/gke-cluster" project_id = "myproject" - name = "cluster-1" + name = "cluster-dataplane-v2" location = "europe-west1-b" vpc_config = { network = var.vpc.self_link @@ -68,8 +68,36 @@ module "cluster-1" { environment = "dev" } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=dataplane-v2.yaml ``` +### Autopilot Cluster + +```hcl +module "cluster-autopilot" { + source = "./fabric/modules/gke-cluster" + project_id = "myproject" + name = "cluster-autopilot" + location = "europe-west1-b" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = { + pods = "pods" + services = "services" + } + master_authorized_ranges = { + internal-vms = "10.0.0.0/8" + } + master_ipv4_cidr_block = "192.168.0.0/28" + } + enable_features = { + autopilot = true + } +} +# tftest modules=1 resources=1 inventory=autopilot.yaml +``` + + ## Variables diff --git a/tests/modules/gke_cluster/examples/autopilot.yaml b/tests/modules/gke_cluster/examples/autopilot.yaml new file mode 100644 index 000000000..0a5380dbb --- /dev/null +++ b/tests/modules/gke_cluster/examples/autopilot.yaml @@ -0,0 +1,32 @@ +# Copyright 2023 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.cluster-autopilot.google_container_cluster.cluster: + enable_autopilot: true + ip_allocation_policy: + - cluster_secondary_range_name: pods + services_secondary_range_name: services + location: europe-west1-b + master_authorized_networks_config: + - cidr_blocks: + - cidr_block: 10.0.0.0/8 + display_name: internal-vms + name: cluster-autopilot + network: projects/xxx/global/networks/aaa + project: myproject + subnetwork: subnet_self_link + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster/examples/basic.yaml b/tests/modules/gke_cluster/examples/basic.yaml new file mode 100644 index 000000000..fe6648c8d --- /dev/null +++ b/tests/modules/gke_cluster/examples/basic.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 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.cluster-1.google_container_cluster.cluster: + default_max_pods_per_node: 32 + ip_allocation_policy: + - cluster_secondary_range_name: pods + services_secondary_range_name: services + location: europe-west1-b + master_authorized_networks_config: + - cidr_blocks: + - cidr_block: 10.0.0.0/8 + display_name: internal-vms + name: cluster-1 + network: projects/xxx/global/networks/aaa + private_cluster_config: + - enable_private_endpoint: true + enable_private_nodes: true + master_global_access_config: + - enabled: false + master_ipv4_cidr_block: 192.168.0.0/28 + private_endpoint_subnetwork: null + project: myproject + remove_default_node_pool: true + resource_labels: + environment: dev + subnetwork: subnet_self_link + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster/examples/dataplane-v2.yaml b/tests/modules/gke_cluster/examples/dataplane-v2.yaml new file mode 100644 index 000000000..ef7ca642f --- /dev/null +++ b/tests/modules/gke_cluster/examples/dataplane-v2.yaml @@ -0,0 +1,45 @@ +# Copyright 2023 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.cluster-1.google_container_cluster.cluster: + datapath_provider: ADVANCED_DATAPATH + ip_allocation_policy: + - cluster_secondary_range_name: pods + services_secondary_range_name: services + location: europe-west1-b + master_authorized_networks_config: + - cidr_blocks: + - cidr_block: 10.0.0.0/8 + display_name: internal-vms + min_master_version: null + name: cluster-dataplane-v2 + network: projects/xxx/global/networks/aaa + private_cluster_config: + - enable_private_endpoint: true + enable_private_nodes: true + master_global_access_config: + - enabled: false + master_ipv4_cidr_block: 192.168.0.0/28 + private_endpoint_subnetwork: null + project: myproject + remove_default_node_pool: true + resource_labels: + environment: dev + subnetwork: subnet_self_link + workload_identity_config: + - workload_pool: myproject.svc.id.goog + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster/fixture/main.tf b/tests/modules/gke_cluster/fixture/main.tf deleted file mode 100644 index 5e11fbd77..000000000 --- a/tests/modules/gke_cluster/fixture/main.tf +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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/gke-cluster" - project_id = "my-project" - name = "cluster-1" - location = "europe-west1-b" - vpc_config = { - network = "mynetwork" - subnetwork = "mysubnet" - } - enable_addons = var.enable_addons - enable_features = var.enable_features - tags = var.tags -} diff --git a/tests/modules/gke_cluster/fixture/variables.tf b/tests/modules/gke_cluster/fixture/variables.tf deleted file mode 100644 index 2104e452b..000000000 --- a/tests/modules/gke_cluster/fixture/variables.tf +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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 "enable_addons" { - type = any - default = { - horizontal_pod_autoscaling = true - http_load_balancing = true - } -} - -variable "enable_features" { - type = any - default = { - workload_identity = true - } -} - -variable "monitoring_config" { - type = any - default = { - managed_prometheus = true - } -} - -variable "tags" { - description = "Network tags applied to nodes." - type = list(string) - default = null -} diff --git a/tests/modules/gke_cluster/test_plan.py b/tests/modules/gke_cluster/test_plan.py deleted file mode 100644 index acd97bede..000000000 --- a/tests/modules/gke_cluster/test_plan.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - - -def test_standard(plan_runner): - "Test resources created with variable defaults." - _, resources = plan_runner() - assert len(resources) == 1 - - cluster_config = resources[0]['values'] - assert cluster_config['name'] == "cluster-1" - assert cluster_config['network'] == "mynetwork" - assert cluster_config['subnetwork'] == "mysubnet" - assert cluster_config['enable_autopilot'] is None - # assert 'service_account' not in node_config - - -def test_autopilot(plan_runner): - "Test resources created with variable defaults." - _, resources = plan_runner(enable_features='{ autopilot=true }') - assert len(resources) == 1 - cluster_config = resources[0]['values'] - assert cluster_config['name'] == "cluster-1" - assert cluster_config['network'] == "mynetwork" - assert cluster_config['subnetwork'] == "mysubnet" - assert cluster_config['enable_autopilot'] == True - # assert 'service_account' not in node_config From 44724f3839e118c8676978b7b62153f64d716067 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 17:46:06 +0100 Subject: [PATCH 12/19] Update plan_summary to support running documentation examples --- tools/plan_summary.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tools/plan_summary.py b/tools/plan_summary.py index def79adb4..78c5f939f 100755 --- a/tools/plan_summary.py +++ b/tools/plan_summary.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import click import sys +import tempfile import yaml from pathlib import Path @@ -27,17 +28,32 @@ import fixtures @click.command() +@click.option('--example', default=False, is_flag=True) @click.argument('module', type=click.Path(), nargs=1) @click.argument('tfvars', type=click.Path(exists=True), nargs=-1) -def main(module, tfvars): - module = BASEDIR / module - summary = fixtures.plan_summary(module, Path(), tfvars) - print(yaml.dump({'values': summary.values})) - print(yaml.dump({'counts': summary.counts})) - outputs = { - k: v.get('value', '__missing__') for k, v in summary.outputs.items() - } - print(yaml.dump({'outputs': outputs})) +def main(example, module, tfvars): + try: + if example: + tmp_dir = tempfile.TemporaryDirectory() + tmp_path = Path(tmp_dir.name) + common_vars = BASEDIR / 'tests' / 'examples' / 'variables.tf' + (tmp_path / 'main.tf').symlink_to(module) + (tmp_path / 'variables.tf').symlink_to(common_vars) + (tmp_path / 'fabric').symlink_to(BASEDIR) + module = tmp_path + else: + module = BASEDIR / module + + summary = fixtures.plan_summary(module, Path(), tfvars) + print(yaml.dump({'values': summary.values})) + print(yaml.dump({'counts': summary.counts})) + outputs = { + k: v.get('value', '__missing__') for k, v in summary.outputs.items() + } + print(yaml.dump({'outputs': outputs})) + finally: + if example: + tmp_dir.cleanup() if __name__ == '__main__': From 1820269680b8847ca1241dfa34b377c7940af844 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 18:19:34 +0100 Subject: [PATCH 13/19] Add inventories to gke-nodepool examples --- modules/gke-nodepool/README.md | 67 ++++++++++----- modules/gke-nodepool/variables.tf | 6 +- .../modules/gke_nodepool/examples/basic.yaml | 23 +++++ .../modules/gke_nodepool/examples/config.yaml | 59 +++++++++++++ .../gke_nodepool/examples/create-sa.yaml | 52 +++++++++++ .../gke_nodepool/examples/external-sa.yaml | 43 ++++++++++ tests/modules/gke_nodepool/fixture/main.tf | 45 ---------- .../modules/gke_nodepool/fixture/variables.tf | 86 ------------------- tests/modules/gke_nodepool/test_plan.py | 67 --------------- 9 files changed, 227 insertions(+), 221 deletions(-) create mode 100644 tests/modules/gke_nodepool/examples/basic.yaml create mode 100644 tests/modules/gke_nodepool/examples/config.yaml create mode 100644 tests/modules/gke_nodepool/examples/create-sa.yaml create mode 100644 tests/modules/gke_nodepool/examples/external-sa.yaml delete mode 100644 tests/modules/gke_nodepool/fixture/main.tf delete mode 100644 tests/modules/gke_nodepool/fixture/variables.tf delete mode 100644 tests/modules/gke_nodepool/test_plan.py diff --git a/modules/gke-nodepool/README.md b/modules/gke-nodepool/README.md index 50e9d08cb..9b42c9b93 100644 --- a/modules/gke-nodepool/README.md +++ b/modules/gke-nodepool/README.md @@ -16,7 +16,7 @@ module "cluster-1-nodepool-1" { location = "europe-west1-b" name = "nodepool-1" } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=basic.yaml ``` ### Internally managed service account @@ -27,22 +27,11 @@ If you create a new service account, its resource and email (in both plain and I #### GCE default service account -To use the GCE default service account, you can ignore the variable which is equivalent to `{ create = null, email = null }`. - -```hcl -module "cluster-1-nodepool-1" { - source = "./fabric/modules/gke-nodepool" - project_id = "myproject" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" -} -# tftest modules=1 resources=1 -``` +To use the GCE default service account, you can ignore the variable which is equivalent to `{ create = null, email = null }`. This is what the first example of this document does. #### Externally defined service account -To use an existing service account, pass in just the `email` attribute. +To use an existing service account, pass in just the `email` attribute. If you do this, will most likely want to use the `cloud-platform` scope. ```hcl module "cluster-1-nodepool-1" { @@ -52,10 +41,11 @@ module "cluster-1-nodepool-1" { location = "europe-west1-b" name = "nodepool-1" service_account = { - email = "foo-bar@myproject.iam.gserviceaccount.com" + email = "foo-bar@myproject.iam.gserviceaccount.com" + oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=external-sa.yaml ``` #### Auto-created service account @@ -70,13 +60,50 @@ module "cluster-1-nodepool-1" { location = "europe-west1-b" name = "nodepool-1" service_account = { - create = true - # optional - email = "spam-eggs" + create = true + email = "spam-eggs" # optional + oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory=create-sa.yaml ``` +### Node & node pool configuration + +```hcl +module "cluster-1-nodepool-1" { + source = "./fabric/modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" + labels = { environment = "dev" } + service_account = { + create = true + email = "nodepool-1" # optional + oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] + } + node_config = { + machine_type = "n2-standard-2" + disk_size_gb = 50 + disk_type = "pd-ssd" + ephemeral_ssd_count = 1 + gvnic = true + spot = true + } + nodepool_config = { + autoscaling = { + max_node_count = 10 + min_node_count = 1 + } + management = { + auto_repair = true + auto_upgrade = false + } + } +} +# tftest modules=1 resources=2 inventory=config.yaml +``` + ## Variables diff --git a/modules/gke-nodepool/variables.tf b/modules/gke-nodepool/variables.tf index e0d3e967a..1166c34f4 100644 --- a/modules/gke-nodepool/variables.tf +++ b/modules/gke-nodepool/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -165,8 +165,8 @@ variable "service_account" { description = "Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used." type = object({ create = optional(bool, false) - email = optional(string, null) - oauth_scopes = optional(list(string), null) + email = optional(string) + oauth_scopes = optional(list(string)) }) default = {} nullable = false diff --git a/tests/modules/gke_nodepool/examples/basic.yaml b/tests/modules/gke_nodepool/examples/basic.yaml new file mode 100644 index 000000000..010b98cda --- /dev/null +++ b/tests/modules/gke_nodepool/examples/basic.yaml @@ -0,0 +1,23 @@ +# Copyright 2023 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.cluster-1-nodepool-1.google_container_node_pool.nodepool: + cluster: cluster-1 + location: europe-west1-b + name: nodepool-1 + project: myproject + +counts: + google_container_node_pool: 1 diff --git a/tests/modules/gke_nodepool/examples/config.yaml b/tests/modules/gke_nodepool/examples/config.yaml new file mode 100644 index 000000000..858e5ca58 --- /dev/null +++ b/tests/modules/gke_nodepool/examples/config.yaml @@ -0,0 +1,59 @@ +# Copyright 2023 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.cluster-1-nodepool-1.google_container_node_pool.nodepool: + autoscaling: + - max_node_count: 10 + min_node_count: 1 + total_max_node_count: null + total_min_node_count: null + cluster: cluster-1 + initial_node_count: 1 + location: europe-west1-b + management: + - auto_repair: true + auto_upgrade: false + name: nodepool-1 + node_config: + - boot_disk_kms_key: null + disk_size_gb: 50 + disk_type: pd-ssd + ephemeral_storage_config: + - local_ssd_count: 1 + gcfs_config: [] + gvnic: [] + kubelet_config: [] + labels: + environment: dev + linux_node_config: [] + logging_variant: DEFAULT + machine_type: n2-standard-2 + node_group: null + oauth_scopes: + - https://www.googleapis.com/auth/cloud-platform + preemptible: false + reservation_affinity: [] + resource_labels: null + sandbox_config: [] + spot: true + tags: null + taint: [] + placement_policy: [] + project: myproject + module.cluster-1-nodepool-1.google_service_account.service_account[0]: {} + +counts: + google_container_node_pool: 1 + google_service_account: 1 diff --git a/tests/modules/gke_nodepool/examples/create-sa.yaml b/tests/modules/gke_nodepool/examples/create-sa.yaml new file mode 100644 index 000000000..df1f2f708 --- /dev/null +++ b/tests/modules/gke_nodepool/examples/create-sa.yaml @@ -0,0 +1,52 @@ +# Copyright 2023 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.cluster-1-nodepool-1.google_container_node_pool.nodepool: + cluster: cluster-1 + location: europe-west1-b + name: nodepool-1 + node_config: + - boot_disk_kms_key: null + disk_type: pd-balanced + ephemeral_storage_config: [] + gcfs_config: [] + gvnic: [] + kubelet_config: [] + linux_node_config: [] + logging_variant: DEFAULT + node_group: null + oauth_scopes: + - https://www.googleapis.com/auth/cloud-platform + preemptible: false + reservation_affinity: [] + resource_labels: null + sandbox_config: [] + spot: false + tags: null + taint: [] + placement_policy: [] + project: myproject + timeouts: null + module.cluster-1-nodepool-1.google_service_account.service_account[0]: + account_id: spam-eggs + description: null + disabled: false + display_name: Terraform GKE cluster-1 nodepool-1. + project: myproject + timeouts: null + +counts: + google_container_node_pool: 1 + google_service_account: 1 diff --git a/tests/modules/gke_nodepool/examples/external-sa.yaml b/tests/modules/gke_nodepool/examples/external-sa.yaml new file mode 100644 index 000000000..059593215 --- /dev/null +++ b/tests/modules/gke_nodepool/examples/external-sa.yaml @@ -0,0 +1,43 @@ +# Copyright 2023 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.cluster-1-nodepool-1.google_container_node_pool.nodepool: + cluster: cluster-1 + location: europe-west1-b + name: nodepool-1 + node_config: + - boot_disk_kms_key: null + disk_type: pd-balanced + ephemeral_storage_config: [] + gcfs_config: [] + gvnic: [] + kubelet_config: [] + linux_node_config: [] + logging_variant: DEFAULT + node_group: null + oauth_scopes: + - https://www.googleapis.com/auth/cloud-platform + preemptible: false + reservation_affinity: [] + resource_labels: null + sandbox_config: [] + service_account: foo-bar@myproject.iam.gserviceaccount.com + spot: false + tags: null + taint: [] + project: myproject + +counts: + google_container_node_pool: 1 diff --git a/tests/modules/gke_nodepool/fixture/main.tf b/tests/modules/gke_nodepool/fixture/main.tf deleted file mode 100644 index 4ee274828..000000000 --- a/tests/modules/gke_nodepool/fixture/main.tf +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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. - */ - -resource "google_service_account" "test" { - project = "my-project" - account_id = "gke-nodepool-test" - display_name = "Test Service Account" -} - -module "test" { - source = "../../../../modules/gke-nodepool" - project_id = "my-project" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" - gke_version = var.gke_version - labels = var.labels - max_pods_per_node = var.max_pods_per_node - node_config = var.node_config - node_count = var.node_count - node_locations = var.node_locations - nodepool_config = var.nodepool_config - pod_range = var.pod_range - reservation_affinity = var.reservation_affinity - service_account = { - create = var.service_account_create - email = google_service_account.test.email - } - sole_tenant_nodegroup = var.sole_tenant_nodegroup - tags = var.tags - taints = var.taints -} diff --git a/tests/modules/gke_nodepool/fixture/variables.tf b/tests/modules/gke_nodepool/fixture/variables.tf deleted file mode 100644 index 18376ec53..000000000 --- a/tests/modules/gke_nodepool/fixture/variables.tf +++ /dev/null @@ -1,86 +0,0 @@ -/** - * 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 "gke_version" { - type = string - default = null -} - -variable "labels" { - type = map(string) - default = {} - nullable = false -} - -variable "max_pods_per_node" { - type = number - default = null -} - -variable "node_config" { - type = any - default = { - disk_type = "pd-balanced" - } -} - -variable "node_count" { - type = any - default = { - initial = 1 - } - nullable = false -} - -variable "node_locations" { - type = list(string) - default = null -} - -variable "nodepool_config" { - type = any - default = null -} - -variable "pod_range" { - type = any - default = null -} - -variable "reservation_affinity" { - type = any - default = null -} - -variable "service_account_create" { - type = bool - default = false -} - -variable "sole_tenant_nodegroup" { - type = string - default = null -} - -variable "tags" { - type = list(string) - default = null -} - -variable "taints" { - type = any - default = null -} diff --git a/tests/modules/gke_nodepool/test_plan.py b/tests/modules/gke_nodepool/test_plan.py deleted file mode 100644 index 75d1cc14b..000000000 --- a/tests/modules/gke_nodepool/test_plan.py +++ /dev/null @@ -1,67 +0,0 @@ -# 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. - - -def test_defaults(plan_runner): - "Test resources created with variable defaults." - _, resources = plan_runner() - assert len(resources) == 1 - assert resources[0]['values']['autoscaling'] == [] - - -def test_service_account(plan_runner): - _, resources = plan_runner() - assert len(resources) == 1 - _, resources = plan_runner(service_account_create='true') - assert len(resources) == 2 - assert 'google_service_account' in [r['type'] for r in resources] - - -def test_nodepool_config(plan_runner): - nodepool_config = '''{ - autoscaling = { use_total_nodes = true, max_node_count = 3} - management = {} - upgrade_settings = { max_surge = 3, max_unavailable = 3 } - }''' - _, resources = plan_runner(nodepool_config=nodepool_config) - assert resources[0]['values']['autoscaling'] == [{ - 'location_policy': None, - 'max_node_count': None, - 'min_node_count': None, - 'total_max_node_count': 3, - 'total_min_node_count': None - }] - nodepool_config = '{ autoscaling = { max_node_count = 3} }' - _, resources = plan_runner(nodepool_config=nodepool_config) - assert resources[0]['values']['autoscaling'] == [{ - 'location_policy': None, - 'max_node_count': 3, - 'min_node_count': None, - 'total_max_node_count': None, - 'total_min_node_count': None - }] - - -def test_node_config(plan_runner): - node_config = '''{ - gcfs = true - metadata = { foo = "bar" } - }''' - _, resources = plan_runner(node_config=node_config) - values = resources[0]['values']['node_config'][0] - assert values['gcfs_config'] == [{'enabled': True}] - assert values['metadata'] == { - 'disable-legacy-endpoints': 'true', - 'foo': 'bar' - } From f014ee57948f5651ee5d3dbb65925860952dc757 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 18:35:00 +0100 Subject: [PATCH 14/19] Fix linting --- modules/dns/README.md | 3 --- modules/gke-nodepool/README.md | 3 +-- modules/net-vpc/README.md | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/dns/README.md b/modules/dns/README.md index 4803a13ce..a405ff753 100644 --- a/modules/dns/README.md +++ b/modules/dns/README.md @@ -101,7 +101,6 @@ module "private-dns" { } # tftest modules=1 resources=1 inventory=reverse-zone.yaml ``` - ### Public Zone @@ -120,8 +119,6 @@ module "public-dns" { ``` - - ## Variables | name | description | type | required | default | diff --git a/modules/gke-nodepool/README.md b/modules/gke-nodepool/README.md index 9b42c9b93..2f632c9c7 100644 --- a/modules/gke-nodepool/README.md +++ b/modules/gke-nodepool/README.md @@ -103,7 +103,6 @@ module "cluster-1-nodepool-1" { } # tftest modules=1 resources=2 inventory=config.yaml ``` - ## Variables @@ -124,7 +123,7 @@ module "cluster-1-nodepool-1" { | [nodepool_config](variables.tf#L115) | Nodepool-level configuration. | object({…}) | | null | | [pod_range](variables.tf#L137) | Pod secondary range configuration. | object({…}) | | null | | [reservation_affinity](variables.tf#L154) | Configuration of the desired reservation which instances could take capacity from. | object({…}) | | null | -| [service_account](variables.tf#L164) | Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used. | object({…}) | | {} | +| [service_account](variables.tf#L164) | Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used. | object({…}) | | {} | | [sole_tenant_nodegroup](variables.tf#L175) | Sole tenant node group. | string | | null | | [tags](variables.tf#L181) | Network tags applied to nodes. | list(string) | | null | | [taints](variables.tf#L187) | Kubernetes taints applied to all nodes. | list(object({…})) | | null | diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 002c738d8..dbd855022 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -341,7 +341,6 @@ flow_logs: # enable, set to empty map to use defaults metadata: "INCLUDE_ALL_METADATA" filter_expression: null ``` - ### Custom Routes From 2aee1dd1c70387cb480d58dcb1d002a6d9a1eafb Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 18:43:13 +0100 Subject: [PATCH 15/19] Fix broken link --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18a42f59d..d34ad2d99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -754,7 +754,7 @@ def test_name(plan_summary, tfvars_to_yaml, tmp_path): assert s.values[address]['project'] == 'my-project' ``` -For more examples on how to write python tests, the tests for [`organization`](./tests/modules/organization/test_plan_org_policies.py) and [`net-vpc`](./tests/modules/net_vpc/test_routes.py) modules. +For more examples on how to write python tests, check the tests for the [`organization`](./tests/modules/organization/test_plan_org_policies.py) module. #### Testing documentation examples From 13352779acfb5333972f1e57353b021aabaa8ec6 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Thu, 19 Jan 2023 18:55:30 +0100 Subject: [PATCH 16/19] Fix nodepool test --- tests/modules/gke_nodepool/examples/config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/modules/gke_nodepool/examples/config.yaml b/tests/modules/gke_nodepool/examples/config.yaml index 858e5ca58..fc1682a82 100644 --- a/tests/modules/gke_nodepool/examples/config.yaml +++ b/tests/modules/gke_nodepool/examples/config.yaml @@ -15,7 +15,8 @@ values: module.cluster-1-nodepool-1.google_container_node_pool.nodepool: autoscaling: - - max_node_count: 10 + - location_policy: null + max_node_count: 10 min_node_count: 1 total_max_node_count: null total_min_node_count: null From 8945165bc3e35fff827e78acaa81d3a225620249 Mon Sep 17 00:00:00 2001 From: Miren Esnaola Date: Fri, 13 Jan 2023 12:45:18 +0100 Subject: [PATCH 17/19] Improvements in apigee hybrid-gke: now using workload identity and GLB --- blueprints/apigee/hybrid-gke/README.md | 16 +- blueprints/apigee/hybrid-gke/ansible.tf | 13 +- .../tasks/k8s_service_accounts.yaml | 28 ++ .../roles/apigee-hybrid/tasks/main.yaml | 284 +++++++++++++++--- .../apigee-hybrid/templates/overrides.yaml.j2 | 45 +-- blueprints/apigee/hybrid-gke/apigee.tf | 67 ++++- blueprints/apigee/hybrid-gke/diagram.png | Bin 35836 -> 35869 bytes blueprints/apigee/hybrid-gke/glb.tf | 25 ++ blueprints/apigee/hybrid-gke/main.tf | 9 +- blueprints/apigee/hybrid-gke/mgmt.tf | 10 +- blueprints/apigee/hybrid-gke/outputs.tf | 20 ++ .../templates/deploy-apiproxy.sh.tpl | 7 +- tests/blueprints/apigee/hybrid-gke/basic.yaml | 4 +- 13 files changed, 422 insertions(+), 106 deletions(-) create mode 100644 blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml create mode 100644 blueprints/apigee/hybrid-gke/glb.tf create mode 100644 blueprints/apigee/hybrid-gke/outputs.tf diff --git a/blueprints/apigee/hybrid-gke/README.md b/blueprints/apigee/hybrid-gke/README.md index cee4aec1a..305897162 100644 --- a/blueprints/apigee/hybrid-gke/README.md +++ b/blueprints/apigee/hybrid-gke/README.md @@ -25,20 +25,20 @@ The diagram below depicts the architecture. terraform apply ``` +Create an A record in your DNS registrar to point the environment group hostname to the public IP address returned after the terraform configuration was applied. You might need to wait some time until the certificate is provisioned. + ## Testing the blueprint 2. Deploy an api proxy ``` - ./deploy-apiproxy.sh + ./deploy-apiproxy.sh apis-test ``` -3. In the console check the IP address that has been allocated to the Apigee ingress gateway and send some traffic to the deployed API proxy. +3. Send a request ``` - curl -k -v -H "Host:HOSTNAME" \ - --resolve HOSTNAME:443:IP_ADDRESS \ - https://HOSTNAME/httpbin/headers + curl -v https://HOSTNAME/httpbin/headers ``` @@ -56,4 +56,10 @@ The diagram below depicts the architecture. | [region](variables.tf#L84) | Region. | string | | "europe-west1" | | [zone](variables.tf#L90) | Zone. | string | | "europe-west1-c" | +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [ip_address](outputs.tf#L17) | GLB IP address. | | + diff --git a/blueprints/apigee/hybrid-gke/ansible.tf b/blueprints/apigee/hybrid-gke/ansible.tf index e5a491a3c..b7694ab1f 100644 --- a/blueprints/apigee/hybrid-gke/ansible.tf +++ b/blueprints/apigee/hybrid-gke/ansible.tf @@ -18,12 +18,13 @@ resource "local_file" "vars_file" { content = yamlencode({ - cluster = module.cluster.name - region = var.region - project_id = module.project.project_id - envgroup = local.envgroup - env = local.environment - hostname = var.hostname + cluster = module.cluster.name + region = var.region + project_id = module.project.project_id + envgroups = local.envgroups + environments = local.environments + service_accounts = local.google_sas + ingress_ip_name = local.ingress_ip_name }) filename = "${path.module}/ansible/vars/vars.yaml" file_permission = "0666" diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml new file mode 100644 index 000000000..e74ca1596 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml @@ -0,0 +1,28 @@ +# Copyright 2023 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: Create and annotate k8s service account + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: "{{ k8s_service_account }}" + namespace: apigee + annotations: + iam.gke.io/gcp-service-account: "{{ google_service_account }}@{{ project_id }}.iam.gserviceaccount.com" + with_items: "{{ k8s_service_accounts }}" + loop_control: + loop_var: k8s_service_account \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml index 4b72039b8..0907846fd 100644 --- a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml @@ -1,11 +1,11 @@ # Copyright 2023 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. @@ -19,18 +19,27 @@ --project {{ project_id }} \ --internal-ip -- name: Install cert-manager - shell: > - kubectl apply \ - --validate=false \ - -f https://github.com/jetstack/cert-manager/releases/download/v1.7.2/cert-manager.yaml +- name: Download cert-manager + uri: + url: https://github.com/jetstack/cert-manager/releases/download/v1.7.2/cert-manager.yaml + dest: ~/cert-manager.yaml -- name: Wait until pods are ready in cert-manager namespace - shell: > - kubectl wait --for=condition=ready pods \ - -l app.kubernetes.io/instance=cert-manager \ - -n cert-manager \ - --timeout=90s +- name: Apply metrics-server manifest to the cluster. + kubernetes.core.k8s: + state: present + src: ~/cert-manager.yaml + +- name: + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app.kubernetes.io/instance=cert-manager" + namespace: cert-manager + wait_timeout: 90 + wait_condition: + type: Ready + status: True - name: Fetch apigeectl version uri: @@ -48,7 +57,7 @@ unarchive: src: "~/apigeectl.tar.gz" dest: "~" - remote_src: yes + remote_src: yes - name: Move apigeectl folder shell: > @@ -66,25 +75,69 @@ file: src: ~/apigeectl/{{ item }} dest: "~/hybrid-files/{{ item }}" - state: link + state: link with_items: - tools - config - templates - - plugins + - plugins -- name: Create service accounts - shell: > - ~/hybrid-files/tools/create-service-account -i {{ project_id }} -e non-prod -d ~/hybrid-files/service-accounts +- name: Create apigee namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: apigee -- name: Create certificates +- name: Create k8s service accounts + include_tasks: k8s_service_accounts.yaml + vars: + google_service_account: "{{ item.key }}" + k8s_service_accounts: "{{ item.value }}" + with_dict: "{{ service_accounts }}" + +- name: Set hostnames + set_fact: + hostnames: "{{ hostnames | default([]) + item.value }}" + with_dict: "{{ envgroups }}" + +- name: Create certificate and private key shell: > openssl req \ -nodes \ -new \ -x509 \ - -keyout ~/hybrid-files/certs/{{ envgroup }}.key \ - -out ~/hybrid-files/certs/{{ envgroup }}.cert -subj '/CN='{{ hostname }}'' -days 3650 + -keyout ~/hybrid-files/certs/server.key \ + -out ~/hybrid-files/certs/server.crt \ + -subj "/CN=apigee.com' \ + -addext "subjectAltName={{ hostnames | map('regex_replace', '^', 'DNS:') | join(',') }}"" + -days 3650 + +- name: Read certificate + slurp: + src: ~/hybrid-files/certs/server.crt + register: certificate_output + +- name: Read private ket + slurp: + src: ~/hybrid-files/certs/server.key + register: privatekey_output + +- name: Create secret + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: tls-hybrid-ingress + namespace: apigee + type: kubernetes.io/tls + data: + tls.crt: "{{ certificate_output.content }}" + tls.key: "{{ privatekey_output.content }}" - name: Create overrides.yaml template: @@ -96,48 +149,185 @@ curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type:application/json" \ "https://apigee.googleapis.com/v1/organizations/{{ project_id }}:setSyncAuthorization" \ - -d '{"identities":["'"serviceAccount:apigee-non-prod@{{ project_id }}.iam.gserviceaccount.com"'"]}' + -d '{"identities":["'"serviceAccount:apigee-synchronizer@{{ project_id }}.iam.gserviceaccount.com"'"]}' - name: Dry-run (init) shell: > - ~/apigeectl/apigeectl init -f overrides/overrides.yaml --dry-run=client + ~/apigeectl/apigeectl init -f overrides/overrides.yaml --dry-run=client args: chdir: ~/hybrid-files - name: Install the Apigee deployment services Apigee Deployment Controller and Apigee Admission Webhook. shell: > - ~/apigeectl/apigeectl init -f overrides/overrides.yaml + ~/apigeectl/apigeectl init -f overrides/overrides.yaml args: - chdir: ~/hybrid-files + chdir: ~/hybrid-files -- name: Wait until pods are ready in apigee-system namespace - shell: > - kubectl wait --for=condition=ready pods \ - -l app=apigee-controller \ - -n apigee-system \ - --timeout=300s +- name: Wait for apigee-controller pod to be ready + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app=apigee-controller" + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True -- name: Wait until pods are ready in apigee namespace - shell: > - kubectl wait --for=condition=ready pods \ - -l app=apigee-ingressgateway-manager \ - -n apigee \ - --timeout=300s +- name: Wait for apigee-selfsigned-issuer issuer to be ready + kubernetes.core.k8s_info: + kind: Issuer + wait: yes + name: apigee-selfsigned-issuer + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True + +- name: Wait for apigee-serving-cert certificate to be ready + kubernetes.core.k8s_info: + kind: Certificate + wait: yes + name: apigee-serving-cert + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True + +- name: Wait for apigee-resources-install job to be complete + kubernetes.core.k8s_info: + kind: Job + wait: yes + name: apigee-resources-install + namespace: apigee-system + wait_timeout: 360 + wait_condition: + type: Complete + status: True - name: Dry-run (apply) shell: > - ~/apigeectl/apigeectl apply -f overrides/overrides.yaml --dry-run=client + ~/apigeectl/apigeectl apply -f overrides/overrides.yaml --dry-run=client args: chdir: ~/hybrid-files - name: Install the Apigee runtime components shell: > - ~/apigeectl/apigeectl apply -f overrides/overrides.yaml + ~/apigeectl/apigeectl apply -f overrides/overrides.yaml args: - chdir: ~/hybrid-files + chdir: ~/hybrid-files -- name: Check status of the deployment - shell: > - while [ -n "$(kubectl get pods -n apigee | tail -n +2 | grep -v Running | grep -v Completed)" ]; do sleep 1; done - args: - chdir: ~/hybrid-files \ No newline at end of file +- name: Wait for apigee-runtime pod to be ready + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app=apigee-runtime" + namespace: apigee + wait_timeout: 360 + wait_condition: + type: Ready + status: True + +- name: + kubernetes.core.k8s: + state: present + definition: + apiVersion: apigee.cloud.google.com/v1alpha1 + kind: ApigeeRoute + metadata: + name: apigee-wildcard + namespace: apigee + spec: + hostnames: + - '*' + ports: + - number: 443 + protocol: HTTPS + tls: + credentialName: tls-hybrid-ingress + mode: SIMPLE + selector: + app: apigee-ingressgateway + enableNonSniClient: true + +- name: Create google-managed certificate + kubernetes.core.k8s: + state: present + definition: + apiVersion: networking.gke.io/v1 + kind: ManagedCertificate + metadata: + name: "apigee-cert-hybrid" + namespace: apigee + spec: + domains: "{{ hostnames }}" + +- name: Create backend config + kubernetes.core.k8s: + state: present + definition: + apiVersion: cloud.google.com/v1 + kind: BackendConfig + metadata: + name: apigee-ingress-backendconfig + namespace: apigee + spec: + healthCheck: + requestPath: /healthz/ready + port: 15021 + type: HTTP + logging: + enable: true + sampleRate: 0.5 + +- name: Create service + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Service + metadata: + name: apigee-ingressgateway-hybrid + namespace: apigee + annotations: + cloud.google.com/backend-config: '{"default": "apigee-ingress-backendconfig"}' + cloud.google.com/neg: '{"ingress": true}' + cloud.google.com/app-protocols: '{"https":"HTTPS", "status-port": "HTTP"}' + labels: + app: apigee-ingressgateway-hybrid + spec: + ports: + - name: status-port + port: 15021 + targetPort: 15021 + - name: https + port: 443 + targetPort: 8443 + selector: + app: apigee-ingressgateway + ingress_name: ingress + type: ClusterIP + +- name: Create ingress + kubernetes.core.k8s: + state: present + definition: + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + annotations: + networking.gke.io/managed-certificates: "apigee-cert-hybrid" + kubernetes.io/ingress.global-static-ip-name: "{{ ingress_ip_name }}" + kubernetes.io/ingress.allow-http: "false" + name: xlb-apigee + namespace: apigee + spec: + defaultBackend: + service: + name: apigee-ingressgateway-hybrid + port: + number: 443 \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 index 1c2c09ed8..691cc6d5d 100644 --- a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 @@ -1,29 +1,26 @@ gcp: region: {{ region }} projectID: {{ project_id }} + workloadIdentityEnabled: true k8sCluster: name: {{ cluster }} - region: CLUSTER_LOCATION # Must be the closest Google Cloud region to your cluster. + region: {{ region }} # Must be the closest Google Cloud region to your cluster. org: {{ project_id }} -instanceID: "instance-1" +instanceID: "{{ cluster }}-{{ region }}" cassandra: hostNetwork: false - # Set to false for single region installations and multi-region installations - # with connectivity between pods in different clusters, for example GKE installations. - # Set to true for multi-region installations with no communication between - # pods in different clusters, for example GKE On-prem, GKE on AWS, Anthos on bare metal, - # AKS, EKS, and OpenShift installations. - # See Multi-region deployment: Prerequisites virtualhosts: - - name: {{ envgroup }} +{% for k in envgroups %} + - name: {{ k }} + sslSecret: tls-hybrid-ingress + additionalGateways: ["apigee-wildcard"] selector: app: apigee-ingressgateway - sslCertPath: ./certs/{{ envgroup }}.cert - sslKeyPath: ./certs/{{ envgroup }}.key +{% endfor %} ao: args: @@ -37,27 +34,9 @@ ingressGateways: replicaCountMax: 10 envs: - - name: {{ env }} - serviceAccountPaths: - synchronizer: ./service-accounts/{{ project_id }}-apigee-non-prod.json - udca: ./service-accounts/{{ project_id }}-apigee-non-prod.json - runtime: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -mart: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -connectAgent: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -metrics: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -udca: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -watcher: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json +{% for k in environments %} + - name: {{ k }} +{% endfor %} logger: - enabled: true - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json + enabled: false diff --git a/blueprints/apigee/hybrid-gke/apigee.tf b/blueprints/apigee/hybrid-gke/apigee.tf index e3dc6b2e6..b92592aab 100644 --- a/blueprints/apigee/hybrid-gke/apigee.tf +++ b/blueprints/apigee/hybrid-gke/apigee.tf @@ -15,8 +15,51 @@ */ locals { - envgroup = "test" - environment = "apis-test" + envgroups = { + test = [var.hostname] + } + environments = { + apis-test = { + envgroups = ["test"] + } + } + org_short_name = (length(module.project.project_id) < 16 ? + module.project.project_id : + substr(module.project.project_id, 0, 15)) + org_hash = format("%s-%s", local.org_short_name, substr(sha256(module.project.project_id), 0, 7)) + org_env_hashes = { + for k, v in local.environments : + k => format("%s-%s-%s", local.org_short_name, length(k) < 16 ? k : substr(k, 0, 15), substr(sha256("${module.project.project_id}:${k}"), 0, 7)) + } + google_sas = { + apigee-metrics = [ + "apigee-metrics-sa" + ] + apigee-cassandra = [ + "apigee-cassandra-schema-setup-${local.org_hash}-sa", + "apigee-cassandra-user-setup-${local.org_hash}-sa" + ] + apigee-mart = [ + "apigee-mart-${local.org_hash}-sa", + "apigee-connect-agent-${local.org_hash}-sa" + ] + apigee-watcher = [ + "apigee-watcher-${local.org_hash}-sa" + ] + apigee-udca = concat([ + "apigee-udca-${local.org_hash}-sa" + ], + [for k, v in local.org_env_hashes : + "apigee-udca-${local.org_env_hashes[k]}-sa" + ]) + apigee-synchronizer = [ + for k, v in local.org_env_hashes : + "apigee-synchronizer-${local.org_env_hashes[k]}-sa" + ] + apigee-runtime = [for k, v in local.org_env_hashes : + "apigee-runtime-${local.org_env_hashes[k]}-sa" + ] + } } module "apigee" { @@ -26,20 +69,24 @@ module "apigee" { analytics_region = var.region runtime_type = "HYBRID" } - envgroups = { - (local.envgroup) = [var.hostname] - } - environments = { - (local.environment) = { - envgroups = [local.envgroup] - } + envgroups = local.envgroups + environments = local.environments +} + +module "sas" { + for_each = local.google_sas + source = "../../../modules/iam-service-account" + project_id = module.project.project_id + name = each.key + # authoritative roles granted *on* the service accounts to other identities + iam = { + "roles/iam.workloadIdentityUser" = [for v in each.value : "serviceAccount:${module.project.project_id}.svc.id.goog[apigee/${v}]"] } } resource "local_file" "deploy_apiproxy_file" { content = templatefile("${path.module}/templates/deploy-apiproxy.sh.tpl", { org = module.project.project_id - env = local.environment }) filename = "${path.module}/deploy-apiproxy.sh" file_permission = "0777" diff --git a/blueprints/apigee/hybrid-gke/diagram.png b/blueprints/apigee/hybrid-gke/diagram.png index 6d5c2d6bc9f38f962c7ed2967981b0f81a485cd7..57e07ca307ee61b9ce335db27d800b68b9b995ff 100644 GIT binary patch literal 35869 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 N�eHJN56q{{vnS9n=5- literal 35836 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 Date: Fri, 20 Jan 2023 13:55:53 +0100 Subject: [PATCH 18/19] Updated hybrid GKE readme --- blueprints/apigee/hybrid-gke/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blueprints/apigee/hybrid-gke/README.md b/blueprints/apigee/hybrid-gke/README.md index 305897162..ae5c03648 100644 --- a/blueprints/apigee/hybrid-gke/README.md +++ b/blueprints/apigee/hybrid-gke/README.md @@ -25,7 +25,11 @@ The diagram below depicts the architecture. terraform apply ``` -Create an A record in your DNS registrar to point the environment group hostname to the public IP address returned after the terraform configuration was applied. You might need to wait some time until the certificate is provisioned. + Create an A record in your DNS registrar to point the environment group hostname to the public IP address returned after the terraform configuration was applied. You might need to wait some time until the certificate is provisioned. + +5. Install Apigee hybrid using de ansible playbook that is in the ansible folder by running this command + + ansible-playbook playbook.yaml -vvvß ## Testing the blueprint From 3cca689792c16079a991a91c1c85be78764fec18 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Sat, 21 Jan 2023 17:17:51 +0100 Subject: [PATCH 19/19] Check linting for Python dashboard files (#1107) * enable Python lint check for network dashboard * fix linting for network dashboard --- .github/workflows/linting.yml | 5 ++++- blueprints/cloud-operations/network-dashboard/src/main.py | 4 ++-- .../network-dashboard/src/plugins/discover-cai.py | 8 ++++---- .../network-dashboard/src/plugins/series-psa.py | 3 ++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 6d773f9bd..a73d8ae20 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -69,4 +69,7 @@ jobs: - name: Check python formatting id: yapf run: | - yapf --style="{based_on_style: google, indent_width: 2, SPLIT_BEFORE_NAMED_ASSIGNS: false}" -p -d tools/*.py + yapf --style="{based_on_style: google, indent_width: 2, SPLIT_BEFORE_NAMED_ASSIGNS: false}" -p -d \ + tools/*.py \ + blueprints/cloud-operations/network-dashboard/src/*py \ + blueprints/cloud-operations/network-dashboard/src/plugins/*py diff --git a/blueprints/cloud-operations/network-dashboard/src/main.py b/blueprints/cloud-operations/network-dashboard/src/main.py index 6db262a66..bd57f18e4 100755 --- a/blueprints/cloud-operations/network-dashboard/src/main.py +++ b/blueprints/cloud-operations/network-dashboard/src/main.py @@ -84,8 +84,8 @@ def do_discovery(resources): {k: len(v) for k, v in resources.items() if not isinstance(v, str)})) -def do_init(resources, discovery_root, monitoring_project, folders=None, projects=None, - custom_quota=None): +def do_init(resources, discovery_root, monitoring_project, folders=None, + projects=None, custom_quota=None): '''Calls init plugins to configure keys in the shared resource map. Args: diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py index 1d22cd8e9..3041df38a 100644 --- a/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py @@ -62,8 +62,8 @@ def _handle_discovery(resources, response, data): 'Processes the asset API response and returns parsed resources or next URL.' LOGGER.info('discovery handle request') for result in parse_cai_results(data, 'cai-compute', method='list'): - resource = _handle_resource( - resources, result['assetType'], result['resource']) + resource = _handle_resource(resources, result['assetType'], + result['resource']) if not resource: continue yield resource @@ -214,6 +214,7 @@ def _handle_sql_instances(resource, data): 'availabilityType': data['settings']['availabilityType'], } + def _handle_subnetworks(resource, data): 'Handles subnetwork type resource data.' secondary_ranges = [{ @@ -237,8 +238,7 @@ def _self_link(s): def _url(resources): 'Returns discovery URL' discovery_root = resources['config:discovery_root'] - asset_types = '&'.join( - f'assetTypes={t}' for t in TYPES.values()) + asset_types = '&'.join(f'assetTypes={t}' for t in TYPES.values()) return CAI_URL.format(root=discovery_root, asset_types=asset_types) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py index 0ea088f9c..e9993e074 100644 --- a/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py @@ -47,7 +47,8 @@ def timeseries(resources): dtype.endswith('ratio')) psa_nets = { k: ipaddress.ip_network('{}/{}'.format(v['address'], v['prefixLength'])) - for k, v in resources['global_addresses'].items() if v['prefixLength'] + for k, v in resources['global_addresses'].items() + if v['prefixLength'] } psa_counts = {} for address, ip_count in _sql_addresses(resources.get('sql_instances', {})):