diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d3ca09..7cf58eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,61 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - + + +## [26.0.0] - 2023-09-18 + ### BLUEPRINTS +- [[#1684](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1684)] **incompatible change:** Update resource-level IAM interface for kms and pubsub modules ([juliocc](https://github.com/juliocc)) +- [[#1682](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1682)] GKE cluster modules: add optional kube state metrics ([olliefr](https://github.com/olliefr)) +- [[#1681](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1681)] **incompatible change:** Embed subnet-level IAM in the variables controlling creation of subnets ([juliocc](https://github.com/juliocc)) +- [[#1680](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1680)] Upgrades to `monitoring_config` in `gke-cluster-*`, docs update, and cosmetics fixes to GKE cluster modules ([olliefr](https://github.com/olliefr)) +- [[#1679](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1679)] Add lineage on Minimal Data Platform blueprint ([lcaggio](https://github.com/lcaggio)) +- [[#1678](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1678)] Allow only one of `secondary_range_blocks` or `secondary_range_names` when creating GKE clusters. ([juliocc](https://github.com/juliocc)) +- [[#1671](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1671)] **incompatible change:** Fixed, added back environments to each instance, that way we can also… ([apichick](https://github.com/apichick)) +- [[#1662](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1662)] merge labels from data_merges in project factory ([Tutuchan](https://github.com/Tutuchan)) +- [[#1651](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1651)] add AIRFLOW_VAR_ prefix to environment variables in data-platform blueprints ([Tutuchan](https://github.com/Tutuchan)) +- [[#1642](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1642)] New phpIPAM serverless third parties solution in blueprints ([simonebruzzechesse](https://github.com/simonebruzzechesse)) +- [[#1654](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1654)] Fix project factory blueprint and fast stage ([LucaPrete](https://github.com/LucaPrete)) +- [[#1647](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1647)] Bump provider version to 4.80.0 ([juliocc](https://github.com/juliocc)) +- [[#1638](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1638)] gke-cluster-standard: change logging configuration ([olliefr](https://github.com/olliefr)) +- [[#1636](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1636)] Delete api gateway blueprint ([juliodiez](https://github.com/juliodiez)) +- [[#1607](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1607)] Trap requests timeout error in quota sync ([ludoo](https://github.com/ludoo)) - [[#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595)] **incompatible change:** IAM interface refactor ([ludoo](https://github.com/ludoo)) - [[#1601](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1601)] [Data Platform] Update README.md ([lcaggio](https://github.com/lcaggio)) ### DOCUMENTATION +- [[#1687](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1687)] Add IAM variables template to ADR ([juliocc](https://github.com/juliocc)) +- [[#1686](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1686)] CONTRIBUTING guide: fix broken links and update "running tests for specific examples" section ([olliefr](https://github.com/olliefr)) +- [[#1658](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1658)] **incompatible change:** Change type of `iam_bindings` variable to allow multiple conditional bindings ([ludoo](https://github.com/ludoo)) +- [[#1642](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1642)] New phpIPAM serverless third parties solution in blueprints ([simonebruzzechesse](https://github.com/simonebruzzechesse)) +- [[#1640](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1640)] Simplify linting output in workflow ([juliocc](https://github.com/juliocc)) +- [[#1636](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1636)] Delete api gateway blueprint ([juliodiez](https://github.com/juliodiez)) - [[#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595)] **incompatible change:** IAM interface refactor ([ludoo](https://github.com/ludoo)) ### FAST +- [[#1684](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1684)] **incompatible change:** Update resource-level IAM interface for kms and pubsub modules ([juliocc](https://github.com/juliocc)) +- [[#1685](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1685)] Fix psa routing variable in FAST net stages ([ludoo](https://github.com/ludoo)) +- [[#1682](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1682)] GKE cluster modules: add optional kube state metrics ([olliefr](https://github.com/olliefr)) +- [[#1681](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1681)] **incompatible change:** Embed subnet-level IAM in the variables controlling creation of subnets ([juliocc](https://github.com/juliocc)) +- [[#1680](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1680)] Upgrades to `monitoring_config` in `gke-cluster-*`, docs update, and cosmetics fixes to GKE cluster modules ([olliefr](https://github.com/olliefr)) +- [[#1678](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1678)] Allow only one of `secondary_range_blocks` or `secondary_range_names` when creating GKE clusters. ([juliocc](https://github.com/juliocc)) +- [[#1664](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1664)] Align pf stage sample data to new format ([ludoo](https://github.com/ludoo)) +- [[#1663](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1663)] [#1661] Make FAST stage 1 resman tf destroy more reliable ([LucaPrete](https://github.com/LucaPrete)) +- [[#1659](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1659)] Link project factory documentation from FAST stage ([ludoo](https://github.com/ludoo)) +- [[#1658](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1658)] **incompatible change:** Change type of `iam_bindings` variable to allow multiple conditional bindings ([ludoo](https://github.com/ludoo)) +- [[#1654](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1654)] Fix project factory blueprint and fast stage ([LucaPrete](https://github.com/LucaPrete)) +- [[#1638](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1638)] gke-cluster-standard: change logging configuration ([olliefr](https://github.com/olliefr)) +- [[#1634](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1634)] [revert(revert(patch))] Remove unused ASN numbers for CloudNAT in FAST ([LucaPrete](https://github.com/LucaPrete)) +- [[#1631](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1631)] Allow single hfw policy association in folder and organization modules ([juliocc](https://github.com/juliocc)) +- [[#1626](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1626)] Revert "Remove unused ASN numbers from CloudNAT to avoid provider errors" ([LucaPrete](https://github.com/LucaPrete)) +- [[#1623](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1623)] Fix role name for delegated grants in FAST bootstrap ([juliocc](https://github.com/juliocc)) +- [[#1612](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1612)] Fix: align stage-2-e-nva-bgp to the latest APIs ([LucaPrete](https://github.com/LucaPrete)) +- [[#1610](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1610)] Fix: use existing variable to optionally name fw policies ([LucaPrete](https://github.com/LucaPrete)) - [[#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595)] **incompatible change:** IAM interface refactor ([ludoo](https://github.com/ludoo)) - [[#1597](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1597)] fix null object exception in bootstrap output when using cloudsource ([sm3142](https://github.com/sm3142)) - [[#1593](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1593)] Fix FAST CI/CD for Gitlab ([ludoo](https://github.com/ludoo)) @@ -24,6 +66,41 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#1684](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1684)] **incompatible change:** Update resource-level IAM interface for kms and pubsub modules ([juliocc](https://github.com/juliocc)) +- [[#1683](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1683)] Fix subnet iam_bindings to use arbitrary keys ([juliocc](https://github.com/juliocc)) +- [[#1682](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1682)] GKE cluster modules: add optional kube state metrics ([olliefr](https://github.com/olliefr)) +- [[#1681](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1681)] **incompatible change:** Embed subnet-level IAM in the variables controlling creation of subnets ([juliocc](https://github.com/juliocc)) +- [[#1680](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1680)] Upgrades to `monitoring_config` in `gke-cluster-*`, docs update, and cosmetics fixes to GKE cluster modules ([olliefr](https://github.com/olliefr)) +- [[#1678](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1678)] Allow only one of `secondary_range_blocks` or `secondary_range_names` when creating GKE clusters. ([juliocc](https://github.com/juliocc)) +- [[#1675](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1675)] GKE Autopilot module: add network tags ([olliefr](https://github.com/olliefr)) +- [[#1676](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1676)] fixed up nit from PR 1666 ([dgulli](https://github.com/dgulli)) +- [[#1672](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1672)] Added possibility to use gcs push endpoint on pubsub subscription ([luigi-bitonti](https://github.com/luigi-bitonti)) +- [[#1671](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1671)] **incompatible change:** Fixed, added back environments to each instance, that way we can also… ([apichick](https://github.com/apichick)) +- [[#1666](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1666)] added support for global proxy only subnets ([dgulli](https://github.com/dgulli)) +- [[#1669](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1669)] Fix for partner interconnect ([apichick](https://github.com/apichick)) +- [[#1668](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1668)] fix(compute-mig): add correct type optionality for metrics in autosca… ([NotArpit](https://github.com/NotArpit)) +- [[#1667](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1667)] fix(compute-mig): add mode property to compute_region_autoscaler ([NotArpit](https://github.com/NotArpit)) +- [[#1658](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1658)] **incompatible change:** Change type of `iam_bindings` variable to allow multiple conditional bindings ([ludoo](https://github.com/ludoo)) +- [[#1653](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1653)] Fixes to the apigee module ([juliocc](https://github.com/juliocc)) +- [[#1642](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1642)] New phpIPAM serverless third parties solution in blueprints ([simonebruzzechesse](https://github.com/simonebruzzechesse)) +- [[#1650](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1650)] Make net-vpc variables non-nullable ([juliocc](https://github.com/juliocc)) +- [[#1647](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1647)] Bump provider version to 4.80.0 ([juliocc](https://github.com/juliocc)) +- [[#1646](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1646)] gke-cluster-autopilot: add monitoring configuration ([olliefr](https://github.com/olliefr)) +- [[#1645](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1645)] gke-cluster-autopilot: add validation for release_channel input variable ([olliefr](https://github.com/olliefr)) +- [[#1638](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1638)] gke-cluster-standard: change logging configuration ([olliefr](https://github.com/olliefr)) +- [[#1625](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1625)] gke-cluster-autopilot: add logging configuration ([olliefr](https://github.com/olliefr)) +- [[#1637](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1637)] GRPC variable is misnamed "GRCP" in `modules/cloud-run/variables.tf`, causing liveness probe and startup probe to fail ([zacharysmithdatatonic](https://github.com/zacharysmithdatatonic)) +- [[#1632](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1632)] Vpc sc allow null for identity type ([LudovicEmo](https://github.com/LudovicEmo)) +- [[#1633](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1633)] Do not set default ASN number ([LucaPrete](https://github.com/LucaPrete)) +- [[#1631](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1631)] Allow single hfw policy association in folder and organization modules ([juliocc](https://github.com/juliocc)) +- [[#1630](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1630)] [Fix] Add explicit dependency between CR peers and NCC RA spoke creation ([LucaPrete](https://github.com/LucaPrete)) +- [[#1613](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1613)] Cloud SQL activation policy selectable ([cmvalla](https://github.com/cmvalla)) +- [[#1619](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1619)] Adding support for NAT in Apigee ([billabongrob](https://github.com/billabongrob)) +- [[#1620](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1620)] Remove net-firewall-policy match variable validation ([richard-olson](https://github.com/richard-olson)) +- [[#1614](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1614)] Fix net-firewall-policy factory name and action ([richard-olson](https://github.com/richard-olson)) +- [[#1584](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1584)] add support for object upload to gcs module ([ehorning](https://github.com/ehorning)) +- [[#1609](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1609)] **incompatible change:** Use cloud run bindings for cf v2 invoker role, refactor iam handling in cf v2 and cloud run ([ludoo](https://github.com/ludoo)) +- [[#1590](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1590)] GCVE module first release ([eliamaldini](https://github.com/eliamaldini)) - [[#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595)] **incompatible change:** IAM interface refactor ([ludoo](https://github.com/ludoo)) - [[#1600](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1600)] fix(cloud-run): move cpu boost annotation to revision ([LiuVII](https://github.com/LiuVII)) - [[#1599](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1599)] Fixing some typos ([bluPhy](https://github.com/bluPhy)) @@ -38,6 +115,9 @@ All notable changes to this project will be documented in this file. ### TOOLS +- [[#1641](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1641)] Lint script ([juliocc](https://github.com/juliocc)) +- [[#1640](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1640)] Simplify linting output in workflow ([juliocc](https://github.com/juliocc)) +- [[#1635](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1635)] Silence FAST tests warnings ([juliocc](https://github.com/juliocc)) - [[#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595)] **incompatible change:** IAM interface refactor ([ludoo](https://github.com/ludoo)) - [[#1585](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1585)] Print inventory path when a test fails ([juliocc](https://github.com/juliocc)) @@ -1483,7 +1563,8 @@ All notable changes to this project will be documented in this file. - merge development branch with suite of new modules and end-to-end examples -[Unreleased]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v25.0.0...HEAD +[Unreleased]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v26.0.0...HEAD +[26.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v25.0.0...v26.0.0 [25.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v24.0.0...v25.0.0 [24.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v23.0.0...v24.0.0 [23.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v22.0.0...v23.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d997e94..1e12acf7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -686,8 +686,8 @@ Writing `pytest` unit tests to check plan results is really easy, but since wrap In the following sections we describe the three testing approaches we currently have: - [Example-based tests](#testing-via-readmemd-example-blocks): this is perhaps the easiest and most common way to test either a module or a blueprint. You simply have to provide an example call to your module and a few metadata values in the module's README.md. -- [tfvars-based tests](#testing-via-tfvars-and-yaml): allows you to test a module or blueprint by providing variables via tfvar files and an expected plan result in form of an inventory. This type of test is useful, for example, for FAST stages that don't have any examples within their READMEs. -- [Python-based (legacy) tests](#writing-tests-in-python--legacy-approach-): in some situations you might still want to interact directly with `tftest` via Python, if that's the case, use this method to write custom Python logic to test your module in any way you see fit. +- [tfvars-based tests](#testing-via-tfvars-and-yaml-aka-tftest-based-tests): allows you to test a module or blueprint by providing variables via tfvar files and an expected plan result in form of an inventory. This type of test is useful, for example, for FAST stages that don't have any examples within their READMEs. +- [Python-based (legacy) tests](#writing-tests-in-python-legacy-approach): in some situations you might still want to interact directly with `tftest` via Python, if that's the case, use this method to write custom Python logic to test your module in any way you see fit. ### Testing via README.md example blocks @@ -818,27 +818,47 @@ Example-based test are named based on the section within the README.md that cont Here we show a few commonly used selection commands: - Run all examples: - - `pytest tests/examples/` -- Run all examples for modules: - - `pytest -k modules/ tests/examples` + - `pytest tests/examples` +- Run all examples for blueprints only: + - `pytest -k blueprints tests/examples` +- Run all examples for modules only: + - `pytest -k modules tests/examples` - Run all examples for the `net-vpc` module: - - `pytest -k 'net and vpc' tests/examples` -- Run a specific example in module `net-vpc`: - - `pytest -k 'modules and dns and private'` - - `pytest -v 'tests/examples/test_plan.py::test_example[modules/dns:Private Zone]'` + - `pytest -k 'modules and net-vpc:' tests/examples` +- Run a specific example (identified by a substring match on its name) from the `net-vpc` module: + - `pytest -k 'modules and net-vpc: and ipv6' tests/examples` +- Run a specific example (identified by its full name) from the `net-vpc` module: + - `pytest -v 'tests/examples/test_plan.py::test_example[modules/net-vpc:IPv6:1]'` - Run tests for all blueprints except those under the gke directory: - - `pytest -k 'blueprints and not gke'` + - `pytest -k 'blueprints and not gke' tests/examples` -Tip: you can use `pytest --collect-only` to fine tune your selection query without actually running the tests. Once you find the expression matching your desired tests, remove the `collect-only` flag. +> [!NOTE] +> The colon symbol (`:`) in `pytest` keyword expression `'modules and net-vpc:'` makes sure that `net-vpc` is matched but `net-vpc-firewall` or `net-vpc-peering` are not. + +Tip: to list all tests matched by your keyword expression (`-k ...`) without actually running them, you can use the `--collect-only` flag. + +The following command executes a dry run that *lists* all example-based tests for the `gke-cluster-autopilot` module: + +```bash +pytest -k 'modules and gke-cluster-autopilot:' tests/examples --collect-only +``` + +Once you find the expression matching your desired test(s), remove the `--collect-only` flag. + +The next command executes an example-based test found in the *Monitoring Configuration* section of the README file for the `gke-cluster-autopilot` module. That section actually has two tests, so the `:2` part selects the second test only: + +```bash +pytest -k 'modules and gke-cluster-autopilot: and monitoring and :2' tests/examples +``` #### Generating the inventory automatically Building an inventory file by hand is difficult. To simplify this task, the default test runner for examples prints the inventory for the full plan if it succeeds. Therefore, you can start without an inventory and then run a test to get the full plan and extract the pieces you want to build the inventory file. -Suppose you want to generate the inventory for the last DNS example above (the one creating the recordsets from a YAML file). Assuming that example is under the "Private Zone" section in the README for the `dns`, you can run the following command to build the inventory: +Suppose you want to generate the inventory for the last DNS example above (the one creating the recordsets from a YAML file). Assuming that example is the first code block under the "Private Zone" section in the README for the `dns` module, you can run the following command to build the inventory: ```bash -pytest -s 'tests/examples/test_plan.py::test_example[modules/dns:Private Zone]' +pytest -s 'tests/examples/test_plan.py::test_example[modules/dns:Private Zone:1]' ``` which will generate a output similar to this: diff --git a/blueprints/apigee/bigquery-analytics/README.md b/blueprints/apigee/bigquery-analytics/README.md index 5261f72e..3eeeaaf7 100644 --- a/blueprints/apigee/bigquery-analytics/README.md +++ b/blueprints/apigee/bigquery-analytics/README.md @@ -53,14 +53,13 @@ Do the following to verify that everything works as expected. 4. At 4am (UTC) every day the Cloud Scheduler will run and will export the analytics to the BigQuery table. Double-check they are there. - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [envgroups](variables.tf#L24) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | ✓ | | -| [environments](variables.tf#L30) | Environments. | map(object({…})) | ✓ | | -| [instances](variables.tf#L46) | Instance. | map(object({…})) | ✓ | | +| [environments](variables.tf#L30) | Environments. | map(object({…})) | ✓ | | +| [instances](variables.tf#L45) | Instance. | map(object({…})) | ✓ | | | [project_id](variables.tf#L91) | Project ID. | string | ✓ | | | [psc_config](variables.tf#L97) | PSC configuration. | map(string) | ✓ | | | [datastore_name](variables.tf#L17) | Datastore. | string | | "gcs" | @@ -74,7 +73,6 @@ Do the following to verify that everything works as expected. | name | description | sensitive | |---|---|:---:| | [ip_address](outputs.tf#L17) | IP address. | | - ## Test @@ -92,13 +90,13 @@ module "test" { environments = { apis-test = { envgroups = ["test"] - regions = ["europe-west1"] } } instances = { europe-west1 = { runtime_ip_cidr_range = "10.0.4.0/22" troubleshooting_ip_cidr_range = "10.1.0.0/28" + environments = ["apis-test"] } } psc_config = { diff --git a/blueprints/apigee/bigquery-analytics/variables.tf b/blueprints/apigee/bigquery-analytics/variables.tf index 53f329b0..3552d58e 100644 --- a/blueprints/apigee/bigquery-analytics/variables.tf +++ b/blueprints/apigee/bigquery-analytics/variables.tf @@ -38,7 +38,6 @@ variable "environments" { })) iam = optional(map(list(string))) envgroups = optional(list(string)) - regions = optional(list(string)) })) nullable = false } @@ -52,6 +51,7 @@ variable "instances" { troubleshooting_ip_cidr_range = string disk_encryption_key = optional(string) consumer_accept_list = optional(list(string)) + environments = optional(list(string)) })) nullable = false } diff --git a/blueprints/apigee/bigquery-analytics/versions.tf b/blueprints/apigee/bigquery-analytics/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/apigee/bigquery-analytics/versions.tf +++ b/blueprints/apigee/bigquery-analytics/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/apigee/hybrid-gke/gke.tf b/blueprints/apigee/hybrid-gke/gke.tf index 6ae38433..701384b9 100644 --- a/blueprints/apigee/hybrid-gke/gke.tf +++ b/blueprints/apigee/hybrid-gke/gke.tf @@ -20,12 +20,9 @@ module "cluster" { name = "cluster" location = var.region vpc_config = { - network = module.vpc.self_link - subnetwork = module.vpc.subnet_self_links["${var.region}/subnet-apigee"] - secondary_range_names = { - pods = "pods" - services = "services" - } + network = module.vpc.self_link + subnetwork = module.vpc.subnet_self_links["${var.region}/subnet-apigee"] + secondary_range_names = {} master_authorized_ranges = var.cluster_network_config.master_authorized_cidr_blocks master_ipv4_cidr_block = var.cluster_network_config.master_cidr_block } @@ -79,4 +76,4 @@ module "apigee-runtime-nodepool" { create = true } tags = ["node"] -} \ No newline at end of file +} diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee.tf index 2923f1f6..afad0f0d 100644 --- a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee.tf +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee.tf @@ -76,11 +76,11 @@ module "apigee" { environments = { (local.environment) = { envgroups = [local.envgroup] - regions = [var.region] } } instances = { (var.region) = { + environments = [local.environment] runtime_ip_cidr_range = var.apigee_runtime_ip_cidr_range troubleshooting_ip_cidr_range = var.apigee_troubleshooting_ip_cidr_range } diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/versions.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/versions.tf +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/adfs/versions.tf b/blueprints/cloud-operations/adfs/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/adfs/versions.tf +++ b/blueprints/cloud-operations/adfs/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf index e4082f69..e396364e 100644 --- a/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf +++ b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf @@ -55,10 +55,12 @@ module "vpc" { } module "pubsub" { - source = "../../../modules/pubsub" - project_id = module.project.project_id - name = var.name - subscriptions = { "${var.name}-default" = null } + source = "../../../modules/pubsub" + project_id = module.project.project_id + name = var.name + subscriptions = { + "${var.name}-default" = {} + } iam = { "roles/pubsub.publisher" = [ "serviceAccount:${module.project.service_accounts.robots.cloudasset}" diff --git a/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf +++ b/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/dns-fine-grained-iam/versions.tf b/blueprints/cloud-operations/dns-fine-grained-iam/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/dns-fine-grained-iam/versions.tf +++ b/blueprints/cloud-operations/dns-fine-grained-iam/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/dns-shared-vpc/versions.tf b/blueprints/cloud-operations/dns-shared-vpc/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/dns-shared-vpc/versions.tf +++ b/blueprints/cloud-operations/dns-shared-vpc/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf b/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf +++ b/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/onprem-sa-key-management/versions.tf b/blueprints/cloud-operations/onprem-sa-key-management/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/onprem-sa-key-management/versions.tf +++ b/blueprints/cloud-operations/onprem-sa-key-management/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/packer-image-builder/versions.tf b/blueprints/cloud-operations/packer-image-builder/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/packer-image-builder/versions.tf +++ b/blueprints/cloud-operations/packer-image-builder/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/quota-monitoring/main.tf b/blueprints/cloud-operations/quota-monitoring/main.tf index f644c8fb..a49891c0 100644 --- a/blueprints/cloud-operations/quota-monitoring/main.tf +++ b/blueprints/cloud-operations/quota-monitoring/main.tf @@ -39,7 +39,7 @@ module "pubsub" { project_id = module.project.project_id name = var.name subscriptions = { - "${var.name}-default" = null + "${var.name}-default" = {} } # the Cloud Scheduler robot service account already has pubsub.topics.publish # at the project level via roles/cloudscheduler.serviceAgent diff --git a/blueprints/cloud-operations/quota-monitoring/versions.tf b/blueprints/cloud-operations/quota-monitoring/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/quota-monitoring/versions.tf +++ b/blueprints/cloud-operations/quota-monitoring/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf index c10c0b6b..6460384e 100644 --- a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf +++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf @@ -63,7 +63,7 @@ module "pubsub" { project_id = module.project.project_id name = var.name subscriptions = { - "${var.name}-default" = null + "${var.name}-default" = {} } # the Cloud Scheduler robot service account already has pubsub.topics.publish # at the project level via roles/cloudscheduler.serviceAgent @@ -74,7 +74,7 @@ module "pubsub_file" { project_id = module.project.project_id name = var.name_cffile subscriptions = { - "${var.name_cffile}-default" = null + "${var.name_cffile}-default" = {} } # the Cloud Scheduler robot service account already has pubsub.topics.publish # at the project level via roles/cloudscheduler.serviceAgent diff --git a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf +++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/data-solutions/bq-ml/versions.tf b/blueprints/data-solutions/bq-ml/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/data-solutions/bq-ml/versions.tf +++ b/blueprints/data-solutions/bq-ml/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/data-solutions/cloudsql-multiregion/README.md b/blueprints/data-solutions/cloudsql-multiregion/README.md index 85f2594c..def4d3f1 100644 --- a/blueprints/data-solutions/cloudsql-multiregion/README.md +++ b/blueprints/data-solutions/cloudsql-multiregion/README.md @@ -179,5 +179,5 @@ module "test" { } prefix = "prefix" } -# tftest modules=9 resources=43 +# tftest modules=9 resources=44 ``` diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf index 27fbe99b..fb446e71 100644 --- a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf +++ b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf @@ -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. @@ -106,7 +106,10 @@ module "kms" { name = "${var.prefix}-${var.region}", location = var.region } - keys = { key-gce = null, key-gcs = null } + keys = { + key-gce = {} + key-gcs = {} + } } ############################################################################### diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf b/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf +++ b/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/data-solutions/composer-2/README.md b/blueprints/data-solutions/composer-2/README.md index c43590e7..6cf927b7 100644 --- a/blueprints/data-solutions/composer-2/README.md +++ b/blueprints/data-solutions/composer-2/README.md @@ -139,5 +139,5 @@ module "test" { } prefix = "prefix" } -# tftest modules=5 resources=28 +# tftest modules=5 resources=29 ``` diff --git a/blueprints/data-solutions/data-platform-foundations/03-composer.tf b/blueprints/data-solutions/data-platform-foundations/03-composer.tf index f806f0e5..8c803e4b 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-composer.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-composer.tf @@ -15,7 +15,7 @@ # tfdoc:file:description Orchestration Cloud Composer definition. locals { - env_variables = { + _env_variables = { BQ_LOCATION = var.location DATA_CAT_TAGS = try(jsonencode(module.common-datacatalog.tags), "{}") DF_KMS_KEY = try(var.service_encryption_keys.dataflow, "") @@ -48,6 +48,12 @@ locals { TRF_SA_DF = module.transf-sa-df-0.email TRF_SA_BQ = module.transf-sa-bq-0.email } + env_variables = { + for k, v in merge( + try(var.composer_config.software_config.env_variables, null), + local._env_variables + ) : "AIRFLOW_VAR_${k}" => v + } } module "orch-sa-cmp-0" { source = "../../../modules/iam-service-account" @@ -70,7 +76,7 @@ resource "google_composer_environment" "orch-cmp-0" { software_config { airflow_config_overrides = try(var.composer_config.software_config.airflow_config_overrides, null) pypi_packages = try(var.composer_config.software_config.pypi_packages, null) - env_variables = merge(try(var.composer_config.software_config.env_variables, null), local.env_variables) + env_variables = local.env_variables image_version = try(var.composer_config.software_config.image_version, null) } dynamic "workloads_config" { diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline.py index a682d346..45b71b30 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline.py +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline.py @@ -16,57 +16,52 @@ # Load The Dependencies # -------------------------------------------------------------------------------- -import csv import datetime -import io -import json -import logging -import os from airflow import models +from airflow.models.variable import Variable from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator -from airflow.operators import dummy -from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator, BigQueryUpsertTableOperator, BigQueryUpdateTableSchemaOperator -from airflow.utils.task_group import TaskGroup +from airflow.operators import empty +from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS")) -DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ") -DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET") -DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS") -DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ") -DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET") -DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS") -DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ") -DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET") -DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS") -DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ") -DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET") -DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS") -GCP_REGION = os.environ.get("GCP_REGION") -DRP_PRJ = os.environ.get("DRP_PRJ") -DRP_BQ = os.environ.get("DRP_BQ") -DRP_GCS = os.environ.get("DRP_GCS") -DRP_PS = os.environ.get("DRP_PS") -LOD_PRJ = os.environ.get("LOD_PRJ") -LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING") -LOD_NET_VPC = os.environ.get("LOD_NET_VPC") -LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET") -LOD_SA_DF = os.environ.get("LOD_SA_DF") -ORC_PRJ = os.environ.get("ORC_PRJ") -ORC_GCS = os.environ.get("ORC_GCS") -TRF_PRJ = os.environ.get("TRF_PRJ") -TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING") -TRF_NET_VPC = os.environ.get("TRF_NET_VPC") -TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET") -TRF_SA_DF = os.environ.get("TRF_SA_DF") -TRF_SA_BQ = os.environ.get("TRF_SA_BQ") -DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "") -DF_REGION = os.environ.get("GCP_REGION") -DF_ZONE = os.environ.get("GCP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +DATA_CAT_TAGS = Variable.get("DATA_CAT_TAGS", deserialize_json=True) +DWH_LAND_PRJ = Variable.get("DWH_LAND_PRJ") +DWH_LAND_BQ_DATASET = Variable.get("DWH_LAND_BQ_DATASET") +DWH_LAND_GCS = Variable.get("DWH_LAND_GCS") +DWH_CURATED_PRJ = Variable.get("DWH_CURATED_PRJ") +DWH_CURATED_BQ_DATASET = Variable.get("DWH_CURATED_BQ_DATASET") +DWH_CURATED_GCS = Variable.get("DWH_CURATED_GCS") +DWH_CONFIDENTIAL_PRJ = Variable.get("DWH_CONFIDENTIAL_PRJ") +DWH_CONFIDENTIAL_BQ_DATASET = Variable.get("DWH_CONFIDENTIAL_BQ_DATASET") +DWH_CONFIDENTIAL_GCS = Variable.get("DWH_CONFIDENTIAL_GCS") +DWH_PLG_PRJ = Variable.get("DWH_PLG_PRJ") +DWH_PLG_BQ_DATASET = Variable.get("DWH_PLG_BQ_DATASET") +DWH_PLG_GCS = Variable.get("DWH_PLG_GCS") +GCP_REGION = Variable.get("GCP_REGION") +DRP_PRJ = Variable.get("DRP_PRJ") +DRP_BQ = Variable.get("DRP_BQ") +DRP_GCS = Variable.get("DRP_GCS") +DRP_PS = Variable.get("DRP_PS") +LOD_PRJ = Variable.get("LOD_PRJ") +LOD_GCS_STAGING = Variable.get("LOD_GCS_STAGING") +LOD_NET_VPC = Variable.get("LOD_NET_VPC") +LOD_NET_SUBNET = Variable.get("LOD_NET_SUBNET") +LOD_SA_DF = Variable.get("LOD_SA_DF") +ORC_PRJ = Variable.get("ORC_PRJ") +ORC_GCS = Variable.get("ORC_GCS") +TRF_PRJ = Variable.get("TRF_PRJ") +TRF_GCS_STAGING = Variable.get("TRF_GCS_STAGING") +TRF_NET_VPC = Variable.get("TRF_NET_VPC") +TRF_NET_SUBNET = Variable.get("TRF_NET_SUBNET") +TRF_SA_DF = Variable.get("TRF_SA_DF") +TRF_SA_BQ = Variable.get("TRF_SA_BQ") +DF_KMS_KEY = Variable.get("DF_KMS_KEY", "") +DF_REGION = Variable.get("GCP_REGION") +DF_ZONE = Variable.get("GCP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -106,12 +101,12 @@ with models.DAG( 'data_pipeline_dag', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py index 56e62897..5e86472a 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py @@ -16,57 +16,53 @@ # Load The Dependencies # -------------------------------------------------------------------------------- -import csv import datetime -import io -import json -import logging -import os from airflow import models +from airflow.models.variable import Variable from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator -from airflow.operators import dummy +from airflow.operators import empty from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator, BigQueryUpsertTableOperator, BigQueryUpdateTableSchemaOperator from airflow.utils.task_group import TaskGroup # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS")) -DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ") -DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET") -DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS") -DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ") -DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET") -DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS") -DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ") -DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET") -DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS") -DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ") -DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET") -DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS") -GCP_REGION = os.environ.get("GCP_REGION") -DRP_PRJ = os.environ.get("DRP_PRJ") -DRP_BQ = os.environ.get("DRP_BQ") -DRP_GCS = os.environ.get("DRP_GCS") -DRP_PS = os.environ.get("DRP_PS") -LOD_PRJ = os.environ.get("LOD_PRJ") -LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING") -LOD_NET_VPC = os.environ.get("LOD_NET_VPC") -LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET") -LOD_SA_DF = os.environ.get("LOD_SA_DF") -ORC_PRJ = os.environ.get("ORC_PRJ") -ORC_GCS = os.environ.get("ORC_GCS") -TRF_PRJ = os.environ.get("TRF_PRJ") -TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING") -TRF_NET_VPC = os.environ.get("TRF_NET_VPC") -TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET") -TRF_SA_DF = os.environ.get("TRF_SA_DF") -TRF_SA_BQ = os.environ.get("TRF_SA_BQ") -DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "") -DF_REGION = os.environ.get("GCP_REGION") -DF_ZONE = os.environ.get("GCP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +DATA_CAT_TAGS = Variable.get("DATA_CAT_TAGS", deserialize_json=True) +DWH_LAND_PRJ = Variable.get("DWH_LAND_PRJ") +DWH_LAND_BQ_DATASET = Variable.get("DWH_LAND_BQ_DATASET") +DWH_LAND_GCS = Variable.get("DWH_LAND_GCS") +DWH_CURATED_PRJ = Variable.get("DWH_CURATED_PRJ") +DWH_CURATED_BQ_DATASET = Variable.get("DWH_CURATED_BQ_DATASET") +DWH_CURATED_GCS = Variable.get("DWH_CURATED_GCS") +DWH_CONFIDENTIAL_PRJ = Variable.get("DWH_CONFIDENTIAL_PRJ") +DWH_CONFIDENTIAL_BQ_DATASET = Variable.get("DWH_CONFIDENTIAL_BQ_DATASET") +DWH_CONFIDENTIAL_GCS = Variable.get("DWH_CONFIDENTIAL_GCS") +DWH_PLG_PRJ = Variable.get("DWH_PLG_PRJ") +DWH_PLG_BQ_DATASET = Variable.get("DWH_PLG_BQ_DATASET") +DWH_PLG_GCS = Variable.get("DWH_PLG_GCS") +GCP_REGION = Variable.get("GCP_REGION") +DRP_PRJ = Variable.get("DRP_PRJ") +DRP_BQ = Variable.get("DRP_BQ") +DRP_GCS = Variable.get("DRP_GCS") +DRP_PS = Variable.get("DRP_PS") +LOD_PRJ = Variable.get("LOD_PRJ") +LOD_GCS_STAGING = Variable.get("LOD_GCS_STAGING") +LOD_NET_VPC = Variable.get("LOD_NET_VPC") +LOD_NET_SUBNET = Variable.get("LOD_NET_SUBNET") +LOD_SA_DF = Variable.get("LOD_SA_DF") +ORC_PRJ = Variable.get("ORC_PRJ") +ORC_GCS = Variable.get("ORC_GCS") +TRF_PRJ = Variable.get("TRF_PRJ") +TRF_GCS_STAGING = Variable.get("TRF_GCS_STAGING") +TRF_NET_VPC = Variable.get("TRF_NET_VPC") +TRF_NET_SUBNET = Variable.get("TRF_NET_SUBNET") +TRF_SA_DF = Variable.get("TRF_SA_DF") +TRF_SA_BQ = Variable.get("TRF_SA_BQ") +DF_KMS_KEY = Variable.get("DF_KMS_KEY", "") +DF_REGION = Variable.get("GCP_REGION") +DF_ZONE = Variable.get("GCP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -106,12 +102,12 @@ with models.DAG( 'data_pipeline_dc_tags_dag', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py index b6784b9e..7bbf67a1 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py @@ -17,12 +17,11 @@ # -------------------------------------------------------------------------------- import datetime -import json -import os import time from airflow import models -from airflow.operators import dummy +from airflow.models.variable import Variable +from airflow.operators import empty from airflow.providers.google.cloud.operators.dataflow import DataflowStartFlexTemplateOperator from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator, BigQueryUpsertTableOperator, BigQueryUpdateTableSchemaOperator from airflow.utils.task_group import TaskGroup @@ -30,42 +29,42 @@ from airflow.utils.task_group import TaskGroup # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS")) -DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ") -DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET") -DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS") -DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ") -DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET") -DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS") -DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ") -DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET") -DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS") -DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ") -DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET") -DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS") -GCP_REGION = os.environ.get("GCP_REGION") -DRP_PRJ = os.environ.get("DRP_PRJ") -DRP_BQ = os.environ.get("DRP_BQ") -DRP_GCS = os.environ.get("DRP_GCS") -DRP_PS = os.environ.get("DRP_PS") -LOD_PRJ = os.environ.get("LOD_PRJ") -LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING") -LOD_NET_VPC = os.environ.get("LOD_NET_VPC") -LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET") -LOD_SA_DF = os.environ.get("LOD_SA_DF") -ORC_PRJ = os.environ.get("ORC_PRJ") -ORC_GCS = os.environ.get("ORC_GCS") -ORC_GCS_TMP_DF = os.environ.get("ORC_GCS_TMP_DF") -TRF_PRJ = os.environ.get("TRF_PRJ") -TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING") -TRF_NET_VPC = os.environ.get("TRF_NET_VPC") -TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET") -TRF_SA_DF = os.environ.get("TRF_SA_DF") -TRF_SA_BQ = os.environ.get("TRF_SA_BQ") -DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "") -DF_REGION = os.environ.get("GCP_REGION") -DF_ZONE = os.environ.get("GCP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +DATA_CAT_TAGS = Variable.get("DATA_CAT_TAGS", deserialize_json=True) +DWH_LAND_PRJ = Variable.get("DWH_LAND_PRJ") +DWH_LAND_BQ_DATASET = Variable.get("DWH_LAND_BQ_DATASET") +DWH_LAND_GCS = Variable.get("DWH_LAND_GCS") +DWH_CURATED_PRJ = Variable.get("DWH_CURATED_PRJ") +DWH_CURATED_BQ_DATASET = Variable.get("DWH_CURATED_BQ_DATASET") +DWH_CURATED_GCS = Variable.get("DWH_CURATED_GCS") +DWH_CONFIDENTIAL_PRJ = Variable.get("DWH_CONFIDENTIAL_PRJ") +DWH_CONFIDENTIAL_BQ_DATASET = Variable.get("DWH_CONFIDENTIAL_BQ_DATASET") +DWH_CONFIDENTIAL_GCS = Variable.get("DWH_CONFIDENTIAL_GCS") +DWH_PLG_PRJ = Variable.get("DWH_PLG_PRJ") +DWH_PLG_BQ_DATASET = Variable.get("DWH_PLG_BQ_DATASET") +DWH_PLG_GCS = Variable.get("DWH_PLG_GCS") +GCP_REGION = Variable.get("GCP_REGION") +DRP_PRJ = Variable.get("DRP_PRJ") +DRP_BQ = Variable.get("DRP_BQ") +DRP_GCS = Variable.get("DRP_GCS") +DRP_PS = Variable.get("DRP_PS") +LOD_PRJ = Variable.get("LOD_PRJ") +LOD_GCS_STAGING = Variable.get("LOD_GCS_STAGING") +LOD_NET_VPC = Variable.get("LOD_NET_VPC") +LOD_NET_SUBNET = Variable.get("LOD_NET_SUBNET") +LOD_SA_DF = Variable.get("LOD_SA_DF") +ORC_PRJ = Variable.get("ORC_PRJ") +ORC_GCS = Variable.get("ORC_GCS") +ORC_GCS_TMP_DF = Variable.get("ORC_GCS_TMP_DF") +TRF_PRJ = Variable.get("TRF_PRJ") +TRF_GCS_STAGING = Variable.get("TRF_GCS_STAGING") +TRF_NET_VPC = Variable.get("TRF_NET_VPC") +TRF_NET_SUBNET = Variable.get("TRF_NET_SUBNET") +TRF_SA_DF = Variable.get("TRF_SA_DF") +TRF_SA_BQ = Variable.get("TRF_SA_BQ") +DF_KMS_KEY = Variable.get("DF_KMS_KEY", "") +DF_REGION = Variable.get("GCP_REGION") +DF_ZONE = Variable.get("GCP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -104,9 +103,9 @@ dataflow_environment = { with models.DAG('data_pipeline_dc_tags_dag_flex', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator(task_id='start', trigger_rule='all_success') + start = empty.EmptyOperator(task_id='start', trigger_rule='all_success') - end = dummy.DummyOperator(task_id='end', trigger_rule='all_success') + end = empty.EmptyOperator(task_id='end', trigger_rule='all_success') # Bigquery Tables created here for demo porpuse. # Consider a dedicated pipeline or tool for a real life scenario. diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py index 34ff10cc..5e60c62f 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py @@ -17,54 +17,53 @@ # -------------------------------------------------------------------------------- import datetime -import json -import os import time from airflow import models +from airflow.models.variable import Variable from airflow.providers.google.cloud.operators.dataflow import DataflowStartFlexTemplateOperator -from airflow.operators import dummy +from airflow.operators import empty from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS")) -DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ") -DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET") -DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS") -DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ") -DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET") -DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS") -DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ") -DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET") -DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS") -DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ") -DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET") -DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS") -GCP_REGION = os.environ.get("GCP_REGION") -DRP_PRJ = os.environ.get("DRP_PRJ") -DRP_BQ = os.environ.get("DRP_BQ") -DRP_GCS = os.environ.get("DRP_GCS") -DRP_PS = os.environ.get("DRP_PS") -LOD_PRJ = os.environ.get("LOD_PRJ") -LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING") -LOD_NET_VPC = os.environ.get("LOD_NET_VPC") -LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET") -LOD_SA_DF = os.environ.get("LOD_SA_DF") -ORC_PRJ = os.environ.get("ORC_PRJ") -ORC_GCS = os.environ.get("ORC_GCS") -ORC_GCS_TMP_DF = os.environ.get("ORC_GCS_TMP_DF") -TRF_PRJ = os.environ.get("TRF_PRJ") -TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING") -TRF_NET_VPC = os.environ.get("TRF_NET_VPC") -TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET") -TRF_SA_DF = os.environ.get("TRF_SA_DF") -TRF_SA_BQ = os.environ.get("TRF_SA_BQ") -DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "") -DF_REGION = os.environ.get("GCP_REGION") -DF_ZONE = os.environ.get("GCP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +DATA_CAT_TAGS = Variable.get("DATA_CAT_TAGS", deserialize_json=True) +DWH_LAND_PRJ = Variable.get("DWH_LAND_PRJ") +DWH_LAND_BQ_DATASET = Variable.get("DWH_LAND_BQ_DATASET") +DWH_LAND_GCS = Variable.get("DWH_LAND_GCS") +DWH_CURATED_PRJ = Variable.get("DWH_CURATED_PRJ") +DWH_CURATED_BQ_DATASET = Variable.get("DWH_CURATED_BQ_DATASET") +DWH_CURATED_GCS = Variable.get("DWH_CURATED_GCS") +DWH_CONFIDENTIAL_PRJ = Variable.get("DWH_CONFIDENTIAL_PRJ") +DWH_CONFIDENTIAL_BQ_DATASET = Variable.get("DWH_CONFIDENTIAL_BQ_DATASET") +DWH_CONFIDENTIAL_GCS = Variable.get("DWH_CONFIDENTIAL_GCS") +DWH_PLG_PRJ = Variable.get("DWH_PLG_PRJ") +DWH_PLG_BQ_DATASET = Variable.get("DWH_PLG_BQ_DATASET") +DWH_PLG_GCS = Variable.get("DWH_PLG_GCS") +GCP_REGION = Variable.get("GCP_REGION") +DRP_PRJ = Variable.get("DRP_PRJ") +DRP_BQ = Variable.get("DRP_BQ") +DRP_GCS = Variable.get("DRP_GCS") +DRP_PS = Variable.get("DRP_PS") +LOD_PRJ = Variable.get("LOD_PRJ") +LOD_GCS_STAGING = Variable.get("LOD_GCS_STAGING") +LOD_NET_VPC = Variable.get("LOD_NET_VPC") +LOD_NET_SUBNET = Variable.get("LOD_NET_SUBNET") +LOD_SA_DF = Variable.get("LOD_SA_DF") +ORC_PRJ = Variable.get("ORC_PRJ") +ORC_GCS = Variable.get("ORC_GCS") +ORC_GCS_TMP_DF = Variable.get("ORC_GCS_TMP_DF") +TRF_PRJ = Variable.get("TRF_PRJ") +TRF_GCS_STAGING = Variable.get("TRF_GCS_STAGING") +TRF_NET_VPC = Variable.get("TRF_NET_VPC") +TRF_NET_SUBNET = Variable.get("TRF_NET_SUBNET") +TRF_SA_DF = Variable.get("TRF_SA_DF") +TRF_SA_BQ = Variable.get("TRF_SA_BQ") +DF_KMS_KEY = Variable.get("DF_KMS_KEY", "") +DF_REGION = Variable.get("GCP_REGION") +DF_ZONE = Variable.get("GCP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -104,9 +103,9 @@ with models.DAG('data_pipeline_dag_flex', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator(task_id='start', trigger_rule='all_success') + start = empty.EmptyOperator(task_id='start', trigger_rule='all_success') - end = dummy.DummyOperator(task_id='end', trigger_rule='all_success') + end = empty.EmptyOperator(task_id='end', trigger_rule='all_success') # Bigquery Tables automatically created for demo purposes. # Consider a dedicated pipeline or tool for a real life scenario. diff --git a/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py b/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py index bade0388..252400ad 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py +++ b/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py @@ -24,49 +24,49 @@ import logging import os from airflow import models -from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator -from airflow.operators import dummy +from airflow.models.variable import Variable +from airflow.operators import empty from airflow.providers.google.cloud.operators.bigquery import BigQueryDeleteTableOperator from airflow.utils.task_group import TaskGroup # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS")) -DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ") -DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET") -DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS") -DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ") -DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET") -DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS") -DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ") -DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET") -DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS") -DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ") -DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET") -DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS") -GCP_REGION = os.environ.get("GCP_REGION") -DRP_PRJ = os.environ.get("DRP_PRJ") -DRP_BQ = os.environ.get("DRP_BQ") -DRP_GCS = os.environ.get("DRP_GCS") -DRP_PS = os.environ.get("DRP_PS") -LOD_PRJ = os.environ.get("LOD_PRJ") -LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING") -LOD_NET_VPC = os.environ.get("LOD_NET_VPC") -LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET") -LOD_SA_DF = os.environ.get("LOD_SA_DF") -ORC_PRJ = os.environ.get("ORC_PRJ") -ORC_GCS = os.environ.get("ORC_GCS") -TRF_PRJ = os.environ.get("TRF_PRJ") -TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING") -TRF_NET_VPC = os.environ.get("TRF_NET_VPC") -TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET") -TRF_SA_DF = os.environ.get("TRF_SA_DF") -TRF_SA_BQ = os.environ.get("TRF_SA_BQ") -DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "") -DF_REGION = os.environ.get("GCP_REGION") -DF_ZONE = os.environ.get("GCP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +DATA_CAT_TAGS = Variable.get("DATA_CAT_TAGS", deserialize_json=True) +DWH_LAND_PRJ = Variable.get("DWH_LAND_PRJ") +DWH_LAND_BQ_DATASET = Variable.get("DWH_LAND_BQ_DATASET") +DWH_LAND_GCS = Variable.get("DWH_LAND_GCS") +DWH_CURATED_PRJ = Variable.get("DWH_CURATED_PRJ") +DWH_CURATED_BQ_DATASET = Variable.get("DWH_CURATED_BQ_DATASET") +DWH_CURATED_GCS = Variable.get("DWH_CURATED_GCS") +DWH_CONFIDENTIAL_PRJ = Variable.get("DWH_CONFIDENTIAL_PRJ") +DWH_CONFIDENTIAL_BQ_DATASET = Variable.get("DWH_CONFIDENTIAL_BQ_DATASET") +DWH_CONFIDENTIAL_GCS = Variable.get("DWH_CONFIDENTIAL_GCS") +DWH_PLG_PRJ = Variable.get("DWH_PLG_PRJ") +DWH_PLG_BQ_DATASET = Variable.get("DWH_PLG_BQ_DATASET") +DWH_PLG_GCS = Variable.get("DWH_PLG_GCS") +GCP_REGION = Variable.get("GCP_REGION") +DRP_PRJ = Variable.get("DRP_PRJ") +DRP_BQ = Variable.get("DRP_BQ") +DRP_GCS = Variable.get("DRP_GCS") +DRP_PS = Variable.get("DRP_PS") +LOD_PRJ = Variable.get("LOD_PRJ") +LOD_GCS_STAGING = Variable.get("LOD_GCS_STAGING") +LOD_NET_VPC = Variable.get("LOD_NET_VPC") +LOD_NET_SUBNET = Variable.get("LOD_NET_SUBNET") +LOD_SA_DF = Variable.get("LOD_SA_DF") +ORC_PRJ = Variable.get("ORC_PRJ") +ORC_GCS = Variable.get("ORC_GCS") +TRF_PRJ = Variable.get("TRF_PRJ") +TRF_GCS_STAGING = Variable.get("TRF_GCS_STAGING") +TRF_NET_VPC = Variable.get("TRF_NET_VPC") +TRF_NET_SUBNET = Variable.get("TRF_NET_SUBNET") +TRF_SA_DF = Variable.get("TRF_SA_DF") +TRF_SA_BQ = Variable.get("TRF_SA_BQ") +DF_KMS_KEY = Variable.get("DF_KMS_KEY", "") +DF_REGION = Variable.get("GCP_REGION") +DF_ZONE = Variable.get("GCP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -106,19 +106,19 @@ with models.DAG( 'delete_tables_dag', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) # Bigquery Tables deleted here for demo porpuse. # Consider a dedicated pipeline or tool for a real life scenario. - with TaskGroup('delete_table') as delte_table: + with TaskGroup('delete_table') as delete_table: delete_table_customers = BigQueryDeleteTableOperator( task_id="delete_table_customers", deletion_dataset_table=DWH_LAND_PRJ+"."+DWH_LAND_BQ_DATASET+".customers", @@ -143,4 +143,4 @@ with models.DAG( impersonation_chain=[TRF_SA_DF] ) - start >> delte_table >> end + start >> delete_table >> end diff --git a/blueprints/data-solutions/data-platform-minimal/01-landing.tf b/blueprints/data-solutions/data-platform-minimal/01-landing.tf index 94ecf5a3..52bf6e8a 100644 --- a/blueprints/data-solutions/data-platform-minimal/01-landing.tf +++ b/blueprints/data-solutions/data-platform-minimal/01-landing.tf @@ -64,6 +64,7 @@ module "land-project" { "bigquerystorage.googleapis.com", "cloudkms.googleapis.com", "cloudresourcemanager.googleapis.com", + "datalineage.googleapis.com", "iam.googleapis.com", "serviceusage.googleapis.com", "stackdriver.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-minimal/02-composer.tf b/blueprints/data-solutions/data-platform-minimal/02-composer.tf index da6fca9a..c250b1fd 100644 --- a/blueprints/data-solutions/data-platform-minimal/02-composer.tf +++ b/blueprints/data-solutions/data-platform-minimal/02-composer.tf @@ -15,7 +15,7 @@ # tfdoc:file:description Cloud Composer resources. locals { - env_variables = { + _env_variables = { BQ_LOCATION = var.location CURATED_BQ_DATASET = module.cur-bq-0.dataset_id CURATED_GCS = module.cur-cs-0.url @@ -31,6 +31,11 @@ locals { PROCESSING_SUBNET = local.processing_subnet PROCESSING_VPC = local.processing_vpc } + env_variables = { + for k, v in merge( + var.composer_config.software_config.env_variables, local._env_variables + ) : "AIRFLOW_VAR_${k}" => v + } } module "processing-sa-cmp-0" { @@ -46,18 +51,20 @@ module "processing-sa-cmp-0" { } resource "google_composer_environment" "processing-cmp-0" { - count = var.enable_services.composer == true ? 1 : 0 - project = module.processing-project.project_id - name = "${var.prefix}-prc-cmp-0" - region = var.region + count = var.enable_services.composer == true ? 1 : 0 + provider = google-beta + project = module.processing-project.project_id + name = "${var.prefix}-prc-cmp-0" + region = var.region config { software_config { airflow_config_overrides = var.composer_config.software_config.airflow_config_overrides pypi_packages = var.composer_config.software_config.pypi_packages - env_variables = merge( - var.composer_config.software_config.env_variables, local.env_variables - ) - image_version = var.composer_config.software_config.image_version + env_variables = local.env_variables + image_version = var.composer_config.software_config.image_version + cloud_data_lineage_integration { + enabled = var.composer_config.software_config.cloud_data_lineage_integration + } } workloads_config { scheduler { diff --git a/blueprints/data-solutions/data-platform-minimal/02-processing.tf b/blueprints/data-solutions/data-platform-minimal/02-processing.tf index 1bba98da..720e2a81 100644 --- a/blueprints/data-solutions/data-platform-minimal/02-processing.tf +++ b/blueprints/data-solutions/data-platform-minimal/02-processing.tf @@ -118,6 +118,7 @@ module "processing-project" { "compute.googleapis.com", "container.googleapis.com", "dataflow.googleapis.com", + "datalineage.googleapis.com", "dataproc.googleapis.com", "iam.googleapis.com", "servicenetworking.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-minimal/03-curated.tf b/blueprints/data-solutions/data-platform-minimal/03-curated.tf index 8bff815f..53a6e7b2 100644 --- a/blueprints/data-solutions/data-platform-minimal/03-curated.tf +++ b/blueprints/data-solutions/data-platform-minimal/03-curated.tf @@ -22,6 +22,7 @@ locals { "cloudkms.googleapis.com", "cloudresourcemanager.googleapis.com", "compute.googleapis.com", + "datalineage.googleapis.com", "iam.googleapis.com", "servicenetworking.googleapis.com", "serviceusage.googleapis.com", diff --git a/blueprints/data-solutions/data-platform-minimal/README.md b/blueprints/data-solutions/data-platform-minimal/README.md index 1f4eb777..62b30acd 100644 --- a/blueprints/data-solutions/data-platform-minimal/README.md +++ b/blueprints/data-solutions/data-platform-minimal/README.md @@ -229,7 +229,7 @@ module "data-platform" { prefix = "myprefix" } -# tftest modules=23 resources=135 +# tftest modules=23 resources=138 ``` ## Customizations @@ -302,19 +302,19 @@ The application layer is out of scope of this script. As a demo purpuse only, on | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_domain](variables.tf#L122) | Organization domain. | string | ✓ | | -| [prefix](variables.tf#L127) | Prefix used for resource names. | string | ✓ | | -| [project_config](variables.tf#L136) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | -| [composer_config](variables.tf#L17) | Cloud Composer config. | object({…}) | | {} | -| [data_catalog_tags](variables.tf#L55) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(object({…})) | | {…} | -| [data_force_destroy](variables.tf#L69) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | bool | | false | -| [enable_services](variables.tf#L75) | Flag to enable or disable services in the Data Platform. | object({…}) | | {} | -| [groups](variables.tf#L84) | User groups. | map(string) | | {…} | -| [location](variables.tf#L94) | Location used for multi-regional resources. | string | | "eu" | -| [network_config](variables.tf#L100) | Shared VPC network configurations to use. If null networks will be created in projects. | object({…}) | | {} | -| [project_suffix](variables.tf#L160) | Suffix used only for project ids. | string | | null | -| [region](variables.tf#L166) | Region used for regional resources. | string | | "europe-west1" | -| [service_encryption_keys](variables.tf#L172) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | {} | +| [organization_domain](variables.tf#L123) | Organization domain. | string | ✓ | | +| [prefix](variables.tf#L128) | Prefix used for resource names. | string | ✓ | | +| [project_config](variables.tf#L137) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | +| [composer_config](variables.tf#L17) | Cloud Composer config. | object({…}) | | {} | +| [data_catalog_tags](variables.tf#L56) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(object({…})) | | {…} | +| [data_force_destroy](variables.tf#L70) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | bool | | false | +| [enable_services](variables.tf#L76) | Flag to enable or disable services in the Data Platform. | object({…}) | | {} | +| [groups](variables.tf#L85) | User groups. | map(string) | | {…} | +| [location](variables.tf#L95) | Location used for multi-regional resources. | string | | "eu" | +| [network_config](variables.tf#L101) | Shared VPC network configurations to use. If null networks will be created in projects. | object({…}) | | {} | +| [project_suffix](variables.tf#L161) | Suffix used only for project ids. | string | | null | +| [region](variables.tf#L167) | Region used for regional resources. | string | | "europe-west1" | +| [service_encryption_keys](variables.tf#L173) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | {} | ## Outputs diff --git a/blueprints/data-solutions/data-platform-minimal/demo/README.md b/blueprints/data-solutions/data-platform-minimal/demo/README.md index b9a24b82..f3c1cbf7 100644 --- a/blueprints/data-solutions/data-platform-minimal/demo/README.md +++ b/blueprints/data-solutions/data-platform-minimal/demo/README.md @@ -54,5 +54,5 @@ source ./env.sh gsutil -i $LND_SA cp demo/data/*.csv gs://$LND_GCS gsutil -i $CMP_SA cp demo/data/*.j* gs://$PRC_GCS gsutil -i $CMP_SA cp demo/pyspark_* gs://$PRC_GCS -gsutil -i $CMP_SA cp demo/dag_*.py $CMP_GCS +gsutil -i $CMP_SA cp demo/dag_*.py gs://$CMP_GCS/dags ``` diff --git a/blueprints/data-solutions/data-platform-minimal/demo/dag_bq_gcs2bq.py b/blueprints/data-solutions/data-platform-minimal/demo/dag_bq_gcs2bq.py index 7abf3691..321071b2 100644 --- a/blueprints/data-solutions/data-platform-minimal/demo/dag_bq_gcs2bq.py +++ b/blueprints/data-solutions/data-platform-minimal/demo/dag_bq_gcs2bq.py @@ -16,34 +16,30 @@ # Load The Dependencies # -------------------------------------------------------------------------------- -import csv import datetime -import io -import json -import logging -import os from airflow import models -from airflow.operators import dummy +from airflow.models.variable import Variable +from airflow.operators import empty from airflow.providers.google.cloud.transfers.gcs_to_bigquery import GCSToBigQueryOperator # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -CURATED_PRJ = os.environ.get("CURATED_PRJ") -CURATED_BQ_DATASET = os.environ.get("CURATED_BQ_DATASET") -CURATED_GCS = os.environ.get("CURATED_GCS") -LAND_PRJ = os.environ.get("LAND_PRJ") -LAND_GCS = os.environ.get("LAND_GCS") -PROCESSING_GCS = os.environ.get("PROCESSING_GCS") -PROCESSING_SA = os.environ.get("PROCESSING_SA") -PROCESSING_PRJ = os.environ.get("PROCESSING_PRJ") -PROCESSING_SUBNET = os.environ.get("PROCESSING_SUBNET") -PROCESSING_VPC = os.environ.get("PROCESSING_VPC") -DP_KMS_KEY = os.environ.get("DP_KMS_KEY", "") -DP_REGION = os.environ.get("DP_REGION") -DP_ZONE = os.environ.get("DP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +CURATED_PRJ = Variable.get("CURATED_PRJ") +CURATED_BQ_DATASET = Variable.get("CURATED_BQ_DATASET") +CURATED_GCS = Variable.get("CURATED_GCS") +LAND_PRJ = Variable.get("LAND_PRJ") +LAND_GCS = Variable.get("LAND_GCS") +PROCESSING_GCS = Variable.get("PROCESSING_GCS") +PROCESSING_SA = Variable.get("PROCESSING_SA") +PROCESSING_PRJ = Variable.get("PROCESSING_PRJ") +PROCESSING_SUBNET = Variable.get("PROCESSING_SUBNET") +PROCESSING_VPC = Variable.get("PROCESSING_VPC") +DP_KMS_KEY = Variable.get("DP_KMS_KEY", "") +DP_REGION = Variable.get("DP_REGION") +DP_ZONE = Variable.get("DP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -73,12 +69,12 @@ with models.DAG( 'bq_gcs2bq', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) @@ -96,7 +92,7 @@ with models.DAG( schema_update_options=['ALLOW_FIELD_RELAXATION', 'ALLOW_FIELD_ADDITION'], schema_object="customers.json", schema_object_bucket=PROCESSING_GCS[5:], - project_id=PROCESSING_PRJ, # The process will continue to run on the dataset project until the Apache Airflow bug is fixed. https://github.com/apache/airflow/issues/32106 + project_id=PROCESSING_PRJ, impersonation_chain=[PROCESSING_SA] ) diff --git a/blueprints/data-solutions/data-platform-minimal/demo/dag_dataflow_gcs2bq.py b/blueprints/data-solutions/data-platform-minimal/demo/dag_dataflow_gcs2bq.py index 6556de8f..111efcdc 100644 --- a/blueprints/data-solutions/data-platform-minimal/demo/dag_dataflow_gcs2bq.py +++ b/blueprints/data-solutions/data-platform-minimal/demo/dag_dataflow_gcs2bq.py @@ -16,36 +16,30 @@ # Load The Dependencies # -------------------------------------------------------------------------------- -import csv import datetime -import io -import json -import logging -import os from airflow import models +from airflow.models.variable import Variable +from airflow.operators import empty from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator -from airflow.operators import dummy -from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator, BigQueryUpsertTableOperator, BigQueryUpdateTableSchemaOperator -from airflow.utils.task_group import TaskGroup # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -CURATED_PRJ = os.environ.get("CURATED_PRJ") -CURATED_BQ_DATASET = os.environ.get("CURATED_BQ_DATASET") -CURATED_GCS = os.environ.get("CURATED_GCS") -LAND_PRJ = os.environ.get("LAND_PRJ") -LAND_GCS = os.environ.get("LAND_GCS") -PROCESSING_GCS = os.environ.get("PROCESSING_GCS") -PROCESSING_SA = os.environ.get("PROCESSING_SA") -PROCESSING_PRJ = os.environ.get("PROCESSING_PRJ") -PROCESSING_SUBNET = os.environ.get("PROCESSING_SUBNET") -PROCESSING_VPC = os.environ.get("PROCESSING_VPC") -DP_KMS_KEY = os.environ.get("DP_KMS_KEY", "") -DP_REGION = os.environ.get("DP_REGION") -DP_ZONE = os.environ.get("DP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +CURATED_PRJ = Variable.get("CURATED_PRJ") +CURATED_BQ_DATASET = Variable.get("CURATED_BQ_DATASET") +CURATED_GCS = Variable.get("CURATED_GCS") +LAND_PRJ = Variable.get("LAND_PRJ") +LAND_GCS = Variable.get("LAND_GCS") +PROCESSING_GCS = Variable.get("PROCESSING_GCS") +PROCESSING_SA = Variable.get("PROCESSING_SA") +PROCESSING_PRJ = Variable.get("PROCESSING_PRJ") +PROCESSING_SUBNET = Variable.get("PROCESSING_SUBNET") +PROCESSING_VPC = Variable.get("PROCESSING_VPC") +DP_KMS_KEY = Variable.get("DP_KMS_KEY", "") +DP_REGION = Variable.get("DP_REGION") +DP_ZONE = Variable.get("DP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -85,12 +79,12 @@ with models.DAG( 'dataflow_gcs2bq', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) diff --git a/blueprints/data-solutions/data-platform-minimal/demo/dag_dataproc_gcs2bq.py b/blueprints/data-solutions/data-platform-minimal/demo/dag_dataproc_gcs2bq.py index a404fa06..3a3dab52 100644 --- a/blueprints/data-solutions/data-platform-minimal/demo/dag_dataproc_gcs2bq.py +++ b/blueprints/data-solutions/data-platform-minimal/demo/dag_dataproc_gcs2bq.py @@ -14,14 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime import time -import os from airflow import models -from airflow.operators import dummy +from airflow.models.variable import Variable +from airflow.operators import empty from airflow.providers.google.cloud.operators.dataproc import ( - DataprocCreateBatchOperator, DataprocDeleteBatchOperator, DataprocGetBatchOperator, DataprocListBatchesOperator + DataprocCreateBatchOperator ) from airflow.utils.dates import days_ago @@ -29,22 +28,21 @@ from airflow.utils.dates import days_ago # -------------------------------------------------------------------------------- # Get variables # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -CURATED_BQ_DATASET = os.environ.get("CURATED_BQ_DATASET") -CURATED_GCS = os.environ.get("CURATED_GCS") -CURATED_PRJ = os.environ.get("CURATED_PRJ") -DP_KMS_KEY = os.environ.get("DP_KMS_KEY", "") -DP_REGION = os.environ.get("DP_REGION") -GCP_REGION = os.environ.get("GCP_REGION") -LAND_PRJ = os.environ.get("LAND_PRJ") -LAND_BQ_DATASET = os.environ.get("LAND_BQ_DATASET") -LAND_GCS = os.environ.get("LAND_GCS") -PHS_CLUSTER_NAME = os.environ.get("PHS_CLUSTER_NAME") -PROCESSING_GCS = os.environ.get("PROCESSING_GCS") -PROCESSING_PRJ = os.environ.get("PROCESSING_PRJ") -PROCESSING_SA = os.environ.get("PROCESSING_SA") -PROCESSING_SUBNET = os.environ.get("PROCESSING_SUBNET") -PROCESSING_VPC = os.environ.get("PROCESSING_VPC") +BQ_LOCATION = Variable.get("BQ_LOCATION") +CURATED_BQ_DATASET = Variable.get("CURATED_BQ_DATASET") +CURATED_GCS = Variable.get("CURATED_GCS") +CURATED_PRJ = Variable.get("CURATED_PRJ") +DP_KMS_KEY = Variable.get("DP_KMS_KEY", "") +DP_REGION = Variable.get("DP_REGION") +LAND_PRJ = Variable.get("LAND_PRJ") +LAND_BQ_DATASET = Variable.get("LAND_BQ_DATASET") +LAND_GCS = Variable.get("LAND_GCS") +PHS_CLUSTER_NAME = Variable.get("PHS_CLUSTER_NAME") +PROCESSING_GCS = Variable.get("PROCESSING_GCS") +PROCESSING_PRJ = Variable.get("PROCESSING_PRJ") +PROCESSING_SA = Variable.get("PROCESSING_SA") +PROCESSING_SUBNET = Variable.get("PROCESSING_SUBNET") +PROCESSING_VPC = Variable.get("PROCESSING_VPC") PYTHON_FILE_LOCATION = PROCESSING_GCS+"/pyspark_gcs2bq.py" PHS_CLUSTER_PATH = "projects/"+PROCESSING_PRJ+"/regions/"+DP_REGION+"/clusters/"+PHS_CLUSTER_NAME @@ -61,12 +59,12 @@ with models.DAG( default_args=default_args, # The interval with which to schedule the DAG schedule_interval=None, # Override to match your needs ) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) diff --git a/blueprints/data-solutions/data-platform-minimal/demo/dag_delete_table.py b/blueprints/data-solutions/data-platform-minimal/demo/dag_delete_table.py index c17c1381..9653cac7 100644 --- a/blueprints/data-solutions/data-platform-minimal/demo/dag_delete_table.py +++ b/blueprints/data-solutions/data-platform-minimal/demo/dag_delete_table.py @@ -16,36 +16,31 @@ # Load The Dependencies # -------------------------------------------------------------------------------- -import csv import datetime -import io -import json -import logging -import os from airflow import models -from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator -from airflow.operators import dummy +from airflow.models.variable import Variable +from airflow.operators import empty from airflow.providers.google.cloud.operators.bigquery import BigQueryDeleteTableOperator from airflow.utils.task_group import TaskGroup # -------------------------------------------------------------------------------- # Set variables - Needed for the DEMO # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -CURATED_PRJ = os.environ.get("CURATED_PRJ") -CURATED_BQ_DATASET = os.environ.get("CURATED_BQ_DATASET") -CURATED_GCS = os.environ.get("CURATED_GCS") -LAND_PRJ = os.environ.get("LAND_PRJ") -LAND_GCS = os.environ.get("LAND_GCS") -PROCESSING_GCS = os.environ.get("PROCESSING_GCS") -PROCESSING_SA = os.environ.get("PROCESSING_SA") -PROCESSING_PRJ = os.environ.get("PROCESSING_PRJ") -PROCESSING_SUBNET = os.environ.get("PROCESSING_SUBNET") -PROCESSING_VPC = os.environ.get("PROCESSING_VPC") -DP_KMS_KEY = os.environ.get("DP_KMS_KEY", "") -DP_REGION = os.environ.get("DP_REGION") -DP_ZONE = os.environ.get("DP_REGION") + "-b" +BQ_LOCATION = Variable.get("BQ_LOCATION") +CURATED_PRJ = Variable.get("CURATED_PRJ") +CURATED_BQ_DATASET = Variable.get("CURATED_BQ_DATASET") +CURATED_GCS = Variable.get("CURATED_GCS") +LAND_PRJ = Variable.get("LAND_PRJ") +LAND_GCS = Variable.get("LAND_GCS") +PROCESSING_GCS = Variable.get("PROCESSING_GCS") +PROCESSING_SA = Variable.get("PROCESSING_SA") +PROCESSING_PRJ = Variable.get("PROCESSING_PRJ") +PROCESSING_SUBNET = Variable.get("PROCESSING_SUBNET") +PROCESSING_VPC = Variable.get("PROCESSING_VPC") +DP_KMS_KEY = Variable.get("DP_KMS_KEY", "") +DP_REGION = Variable.get("DP_REGION") +DP_ZONE = Variable.get("DP_REGION") + "-b" # -------------------------------------------------------------------------------- # Set default arguments @@ -75,23 +70,23 @@ with models.DAG( 'delete_tables_dag', default_args=default_args, schedule_interval=None) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) # Bigquery Tables deleted here for demo porpuse. # Consider a dedicated pipeline or tool for a real life scenario. - with TaskGroup('delete_table') as delte_table: + with TaskGroup('delete_table') as delete_table: delete_table_customers = BigQueryDeleteTableOperator( task_id="delete_table_customers", deletion_dataset_table=CURATED_PRJ+"."+CURATED_BQ_DATASET+".customers", impersonation_chain=[PROCESSING_SA] ) - start >> delte_table >> end + start >> delete_table >> end diff --git a/blueprints/data-solutions/data-platform-minimal/demo/dag_orchestrate_pyspark.py b/blueprints/data-solutions/data-platform-minimal/demo/dag_orchestrate_pyspark.py index 0a68dbc0..4258e7e4 100644 --- a/blueprints/data-solutions/data-platform-minimal/demo/dag_orchestrate_pyspark.py +++ b/blueprints/data-solutions/data-platform-minimal/demo/dag_orchestrate_pyspark.py @@ -14,41 +14,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime import time -import os from airflow import models -from airflow.operators import dummy +from airflow.models.variable import Variable +from airflow.operators import empty from airflow.providers.google.cloud.operators.dataproc import ( - DataprocCreateBatchOperator, DataprocDeleteBatchOperator, DataprocGetBatchOperator, DataprocListBatchesOperator - + DataprocCreateBatchOperator ) from airflow.utils.dates import days_ago # -------------------------------------------------------------------------------- # Get variables # -------------------------------------------------------------------------------- -BQ_LOCATION = os.environ.get("BQ_LOCATION") -CURATED_BQ_DATASET = os.environ.get("CURATED_BQ_DATASET") -CURATED_GCS = os.environ.get("CURATED_GCS") -CURATED_PRJ = os.environ.get("CURATED_PRJ") -DP_KMS_KEY = os.environ.get("DP_KMS_KEY", "") -DP_REGION = os.environ.get("DP_REGION") -GCP_REGION = os.environ.get("GCP_REGION") -LAND_PRJ = os.environ.get("LAND_PRJ") -LAND_BQ_DATASET = os.environ.get("LAND_BQ_DATASET") -LAND_GCS = os.environ.get("LAND_GCS") -PHS_CLUSTER_NAME = os.environ.get("PHS_CLUSTER_NAME") -PROCESSING_GCS = os.environ.get("PROCESSING_GCS") -PROCESSING_PRJ = os.environ.get("PROCESSING_PRJ") -PROCESSING_SA = os.environ.get("PROCESSING_SA") -PROCESSING_SUBNET = os.environ.get("PROCESSING_SUBNET") -PROCESSING_VPC = os.environ.get("PROCESSING_VPC") +BQ_LOCATION = Variable.get("BQ_LOCATION") +CURATED_BQ_DATASET = Variable.get("CURATED_BQ_DATASET") +CURATED_GCS = Variable.get("CURATED_GCS") +CURATED_PRJ = Variable.get("CURATED_PRJ") +DP_KMS_KEY = Variable.get("DP_KMS_KEY", "") +DP_REGION = Variable.get("DP_REGION") +LAND_PRJ = Variable.get("LAND_PRJ") +LAND_BQ_DATASET = Variable.get("LAND_BQ_DATASET") +LAND_GCS = Variable.get("LAND_GCS") +PHS_CLUSTER_NAME = Variable.get("PHS_CLUSTER_NAME") +PROCESSING_GCS = Variable.get("PROCESSING_GCS") +PROCESSING_PRJ = Variable.get("PROCESSING_PRJ") +PROCESSING_SA = Variable.get("PROCESSING_SA") +PROCESSING_SUBNET = Variable.get("PROCESSING_SUBNET") +PROCESSING_VPC = Variable.get("PROCESSING_VPC") -PYTHON_FILE_LOCATION = PROCESSING_GCS+"/pyspark_sort.py" -PHS_CLUSTER_PATH = "projects/"+PROCESSING_PRJ+"/regions/"+DP_REGION+"/clusters/"+PHS_CLUSTER_NAME -BATCH_ID = "batch-create-phs-"+str(int(time.time())) +PYTHON_FILE_LOCATION = PROCESSING_GCS + "/pyspark_sort.py" +PHS_CLUSTER_PATH = f"projects/{PROCESSING_PRJ}/regions/{DP_REGION}/clusters/{PHS_CLUSTER_NAME}" +BATCH_ID = "batch-create-phs-" + str(int(time.time())) default_args = { # Tell airflow to start one day ago, so that it runs as soon as you upload it @@ -60,12 +57,12 @@ with models.DAG( default_args=default_args, # The interval with which to schedule the DAG schedule_interval=None, # Override to match your needs ) as dag: - start = dummy.DummyOperator( + start = empty.EmptyOperator( task_id='start', trigger_rule='all_success' ) - end = dummy.DummyOperator( + end = empty.EmptyOperator( task_id='end', trigger_rule='all_success' ) diff --git a/blueprints/data-solutions/data-platform-minimal/variables.tf b/blueprints/data-solutions/data-platform-minimal/variables.tf index 0dc29003..0bd1deed 100644 --- a/blueprints/data-solutions/data-platform-minimal/variables.tf +++ b/blueprints/data-solutions/data-platform-minimal/variables.tf @@ -19,10 +19,11 @@ variable "composer_config" { type = object({ environment_size = optional(string, "ENVIRONMENT_SIZE_SMALL") software_config = optional(object({ - airflow_config_overrides = optional(map(string), {}) - pypi_packages = optional(map(string), {}) - env_variables = optional(map(string), {}) - image_version = optional(string, "composer-2-airflow-2") + airflow_config_overrides = optional(map(string), {}) + pypi_packages = optional(map(string), {}) + env_variables = optional(map(string), {}) + image_version = optional(string, "composer-2-airflow-2") + cloud_data_lineage_integration = optional(bool, true) }), {}) web_server_access_control = optional(map(string), {}) workloads_config = optional(object({ diff --git a/blueprints/data-solutions/data-playground/versions.tf b/blueprints/data-solutions/data-playground/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/data-solutions/data-playground/versions.tf +++ b/blueprints/data-solutions/data-playground/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf index 5e616630..722016b7 100644 --- a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf +++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf @@ -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,26 +21,27 @@ module "kms" { location = var.region } keys = { - key-df = null - key-gcs = null - key-bq = null - } - key_iam = { - key-gcs = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - "serviceAccount:${module.project.service_accounts.robots.storage}" - ] - }, - key-bq = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - "serviceAccount:${module.project.service_accounts.robots.bq}" - ] - }, key-df = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - "serviceAccount:${module.project.service_accounts.robots.dataflow}", - "serviceAccount:${module.project.service_accounts.robots.compute}", - ] + iam = { + "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ + "serviceAccount:${module.project.service_accounts.robots.dataflow}", + "serviceAccount:${module.project.service_accounts.robots.compute}", + ] + } + } + key-gcs = { + iam = { + "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ + "serviceAccount:${module.project.service_accounts.robots.storage}" + ] + } + } + key-bq = { + iam = { + "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ + "serviceAccount:${module.project.service_accounts.robots.bq}" + ] + } } } } diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/versions.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/versions.tf +++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/data-solutions/shielded-folder/README.md b/blueprints/data-solutions/shielded-folder/README.md index ed177d27..72a6b69f 100644 --- a/blueprints/data-solutions/shielded-folder/README.md +++ b/blueprints/data-solutions/shielded-folder/README.md @@ -159,18 +159,18 @@ terraform apply |---|---|:---:|:---:|:---:| | [access_policy_config](variables.tf#L17) | Provide 'access_policy_create' values if a folder scoped Access Policy creation is needed, uses existing 'policy_name' otherwise. Parent is in 'organizations/123456' format. Policy will be created scoped to the folder. | object({…}) | ✓ | | | [folder_config](variables.tf#L49) | Provide 'folder_create' values if folder creation is needed, uses existing 'folder_id' otherwise. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | -| [organization](variables.tf#L129) | Organization details. | object({…}) | ✓ | | -| [prefix](variables.tf#L137) | Prefix used for resources that need unique names. | string | ✓ | | -| [project_config](variables.tf#L142) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | +| [organization](variables.tf#L148) | Organization details. | object({…}) | ✓ | | +| [prefix](variables.tf#L156) | Prefix used for resources that need unique names. | string | ✓ | | +| [project_config](variables.tf#L161) | Provide 'billing_account_id' value if project creation is needed, uses existing 'project_ids' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | | [data_dir](variables.tf#L29) | Relative path for the folder storing configuration data. | string | | "data" | | [enable_features](variables.tf#L35) | Flag to enable features on the solution. | object({…}) | | {…} | | [groups](variables.tf#L65) | User groups. | object({…}) | | {} | -| [kms_keys](variables.tf#L75) | KMS keys to create, keyed by name. | map(object({…})) | | {} | -| [log_locations](variables.tf#L87) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | -| [log_sinks](variables.tf#L104) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | -| [vpc_sc_access_levels](variables.tf#L162) | VPC SC access level definitions. | map(object({…})) | | {} | -| [vpc_sc_egress_policies](variables.tf#L191) | VPC SC egress policy definitions. | map(object({…})) | | {} | -| [vpc_sc_ingress_policies](variables.tf#L211) | VPC SC ingress policy definitions. | map(object({…})) | | {} | +| [kms_keys](variables.tf#L75) | KMS keys to create, keyed by name. | map(object({…})) | | {} | +| [log_locations](variables.tf#L111) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {} | +| [log_sinks](variables.tf#L123) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | +| [vpc_sc_access_levels](variables.tf#L181) | VPC SC access level definitions. | map(object({…})) | | {} | +| [vpc_sc_egress_policies](variables.tf#L210) | VPC SC egress policy definitions. | map(object({…})) | | {} | +| [vpc_sc_ingress_policies](variables.tf#L230) | VPC SC ingress policy definitions. | map(object({…})) | | {} | ## Outputs diff --git a/blueprints/data-solutions/shielded-folder/kms.tf b/blueprints/data-solutions/shielded-folder/kms.tf index 9953d458..4a634fcc 100644 --- a/blueprints/data-solutions/shielded-folder/kms.tf +++ b/blueprints/data-solutions/shielded-folder/kms.tf @@ -17,12 +17,17 @@ # tfdoc:file:description Security project, Cloud KMS and Secret Manager resources. locals { + # list of locations with keys kms_locations = distinct(flatten([ for k, v in var.kms_keys : v.locations ])) + # map { location -> { key_name -> key_details } } kms_locations_keys = { - for loc in local.kms_locations : loc => { - for k, v in var.kms_keys : k => v if contains(v.locations, loc) + for loc in local.kms_locations : + loc => { + for k, v in var.kms_keys : + k => v + if contains(v.locations, loc) } } kms_log_locations = distinct(flatten([ @@ -30,17 +35,14 @@ locals { ])) kms_log_sink_keys = { "storage" = { - labels = {} locations = [var.log_locations.storage] rotation_period = "7776000s" } "bq" = { - labels = {} locations = [var.log_locations.bq] rotation_period = "7776000s" } "pubsub" = { - labels = {} locations = [var.log_locations.pubsub] rotation_period = "7776000s" } @@ -88,12 +90,6 @@ module "sec-kms" { location = each.key name = "sec-${each.key}" } - key_iam = { - for k, v in local.kms_locations_keys[each.key] : k => v.iam - } - key_iam_bindings_additive = { - for k, v in local.kms_locations_keys[each.key] : k => v.iam_bindings_additive - } keys = local.kms_locations_keys[each.key] } diff --git a/blueprints/data-solutions/shielded-folder/variables.tf b/blueprints/data-solutions/shielded-folder/variables.tf index 5bb80d57..03fea7c4 100644 --- a/blueprints/data-solutions/shielded-folder/variables.tf +++ b/blueprints/data-solutions/shielded-folder/variables.tf @@ -75,11 +75,35 @@ variable "groups" { variable "kms_keys" { description = "KMS keys to create, keyed by name." type = map(object({ - iam = optional(map(list(string)), {}) - iam_bindings_additive = optional(map(map(any)), {}) - labels = optional(map(string), {}) - locations = optional(list(string), ["global", "europe", "europe-west1"]) - rotation_period = optional(string, "7776000s") + labels = optional(map(string)) + locations = optional(list(string), ["global", "europe", "europe-west1"]) + rotation_period = optional(string, "7776000s") + purpose = optional(string, "ENCRYPT_DECRYPT") + skip_initial_version_creation = optional(bool, false) + version_template = optional(object({ + algorithm = string + protection_level = optional(string, "SOFTWARE") + })) + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + })) default = {} } @@ -92,12 +116,7 @@ variable "log_locations" { logging = optional(string, "global") pubsub = optional(string, "global") }) - default = { - bq = "europe" - storage = "europe" - logging = "global" - pubsub = null - } + default = {} nullable = false } diff --git a/blueprints/factories/net-vpc-firewall-yaml/versions.tf b/blueprints/factories/net-vpc-firewall-yaml/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/factories/net-vpc-firewall-yaml/versions.tf +++ b/blueprints/factories/net-vpc-firewall-yaml/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index d144a11a..74682ae9 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -55,6 +55,7 @@ billing_account: 012345-67890A-BCDEF0 labels: app: app-1 team: foo +parent: folders/12345678 service_encryption_key_ids: compute: - projects/kms-central-prj/locations/europe-west3/keyRings/my-keyring/cryptoKeys/europe3-gce @@ -71,6 +72,7 @@ service_accounts: labels: app: app-1 team: foo +parent: folders/12345678 service_accounts: app-2-be: {} @@ -81,10 +83,10 @@ service_accounts: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [factory_data](variables.tf#L83) | Project data from either YAML files or externally parsed data. | object({…}) | ✓ | | -| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | -| [data_merges](variables.tf#L44) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | -| [data_overrides](variables.tf#L63) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | +| [factory_data](variables.tf#L85) | Project data from either YAML files or externally parsed data. | object({…}) | ✓ | | +| [data_defaults](variables.tf#L17) | Optional default values used when corresponding project data from files are missing. | object({…}) | | {} | +| [data_merges](variables.tf#L45) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} | +| [data_overrides](variables.tf#L64) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} | ## Outputs diff --git a/blueprints/factories/project-factory/factory.tf b/blueprints/factories/project-factory/factory.tf index dac843df..0390b055 100644 --- a/blueprints/factories/project-factory/factory.tf +++ b/blueprints/factories/project-factory/factory.tf @@ -28,11 +28,11 @@ locals { ) projects = { for k, v in local._data : k => merge(v, { - billing_account = coalesce( + billing_account = try(coalesce( var.data_overrides.billing_account, try(v.billing_account, null), var.data_defaults.billing_account - ) + ), null) contacts = coalesce( var.data_overrides.contacts, try(v.contacts, null), @@ -46,6 +46,11 @@ locals { try(v.metric_scopes, null), var.data_defaults.metric_scopes ) + parent = coalesce( + var.data_overrides.parent, + try(v.parent, null), + var.data_defaults.parent + ) prefix = coalesce( var.data_overrides.prefix, try(v.prefix, null), diff --git a/blueprints/factories/project-factory/main.tf b/blueprints/factories/project-factory/main.tf index 7d173a11..9a230063 100644 --- a/blueprints/factories/project-factory/main.tf +++ b/blueprints/factories/project-factory/main.tf @@ -33,11 +33,13 @@ module "projects" { iam = try(each.value.iam, {}) iam_bindings = try(each.value.iam_bindings, {}) iam_bindings_additive = try(each.value.iam_bindings_additive, {}) - labels = each.value.labels - lien_reason = try(each.value.lien_reason, null) - logging_data_access = try(each.value.logging_data_access, {}) - logging_exclusions = try(each.value.logging_exclusions, {}) - logging_sinks = try(each.value.logging_sinks, {}) + labels = merge( + each.value.labels, var.data_merges.labels + ) + lien_reason = try(each.value.lien_reason, null) + logging_data_access = try(each.value.logging_data_access, {}) + logging_exclusions = try(each.value.logging_exclusions, {}) + logging_sinks = try(each.value.logging_sinks, {}) metric_scopes = distinct(concat( each.value.metric_scopes, var.data_merges.metric_scopes )) diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index 67917846..d7176474 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/variables.tf @@ -21,6 +21,7 @@ variable "data_defaults" { contacts = optional(map(list(string)), {}) labels = optional(map(string), {}) metric_scopes = optional(list(string), []) + parent = optional(string) prefix = optional(string) service_encryption_key_ids = optional(map(list(string)), {}) service_perimeter_bridges = optional(list(string), []) @@ -65,6 +66,7 @@ variable "data_overrides" { type = object({ billing_account = optional(string) contacts = optional(map(list(string))) + parent = optional(string) prefix = optional(string) service_encryption_key_ids = optional(map(list(string))) service_perimeter_bridges = optional(list(string)) diff --git a/blueprints/gke/autopilot/cluster.tf b/blueprints/gke/autopilot/cluster.tf index ed6fa661..49409c44 100644 --- a/blueprints/gke/autopilot/cluster.tf +++ b/blueprints/gke/autopilot/cluster.tf @@ -20,12 +20,9 @@ module "cluster" { name = "cluster" location = var.region vpc_config = { - network = module.vpc.self_link - subnetwork = module.vpc.subnet_self_links["${var.region}/subnet-cluster"] - secondary_range_names = { - pods = "pods" - services = "services" - } + network = module.vpc.self_link + subnetwork = module.vpc.subnet_self_links["${var.region}/subnet-cluster"] + secondary_range_names = {} master_authorized_ranges = var.cluster_network_config.master_authorized_cidr_blocks master_ipv4_cidr_block = var.cluster_network_config.master_cidr_block } @@ -33,8 +30,17 @@ module "cluster" { # autopilot = true # } # monitoring_config = { - # enenable_components = ["SYSTEM_COMPONENTS"] - # managed_prometheus = true + # # (Optional) control plane metrics + # enable_api_server_metrics = true + # enable_controller_manager_metrics = true + # enable_scheduler_metrics = true + # # (Optional) kube state metrics + # enable_daemonset_metrics = true + # enable_deployment_metrics = true + # enable_hpa_metrics = true + # enable_pod_metrics = true + # enable_statefulset_metrics = true + # enable_storage_metrics = true # } # cluster_autoscaling = { # auto_provisioning_defaults = { @@ -51,4 +57,4 @@ module "node_sa" { source = "../../../modules/iam-service-account" project_id = module.project.project_id name = "sa-node" -} \ No newline at end of file +} diff --git a/blueprints/gke/binauthz/main.tf b/blueprints/gke/binauthz/main.tf index 2eac7c56..8cff68a0 100644 --- a/blueprints/gke/binauthz/main.tf +++ b/blueprints/gke/binauthz/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. @@ -115,20 +115,16 @@ module "kms" { project_id = module.project.project_id keyring = { location = var.region, name = "test-keyring" } keyring_create = true - keys = { test-key = null } - key_purpose = { + keys = { test-key = { purpose = "ASYMMETRIC_SIGN" version_template = { - algorithm = "RSA_SIGN_PKCS1_4096_SHA512" - protection_level = null + algorithm = "RSA_SIGN_PKCS1_4096_SHA512" + } + iam = { + "roles/cloudkms.publicKeyViewer" = [module.image_cb_sa.iam_email] + "roles/cloudkms.signer" = [module.image_cb_sa.iam_email] } - } - } - key_iam = { - test-key = { - "roles/cloudkms.publicKeyViewer" = [module.image_cb_sa.iam_email] - "roles/cloudkms.signer" = [module.image_cb_sa.iam_email] } } } diff --git a/blueprints/gke/multitenant-fleet/README.md b/blueprints/gke/multitenant-fleet/README.md index baaf288f..ed89a878 100644 --- a/blueprints/gke/multitenant-fleet/README.md +++ b/blueprints/gke/multitenant-fleet/README.md @@ -244,21 +244,21 @@ module "gke" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [folder_id](variables.tf#L138) | Folder used for the GKE project in folders/nnnnnnnnnnn format. | string | ✓ | | -| [prefix](variables.tf#L189) | Prefix used for resource names. | string | ✓ | | -| [project_id](variables.tf#L198) | ID of the project that will contain all the clusters. | string | ✓ | | -| [vpc_config](variables.tf#L210) | Shared VPC project and VPC details. | object({…}) | ✓ | | -| [clusters](variables.tf#L22) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | -| [fleet_configmanagement_clusters](variables.tf#L76) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | -| [fleet_configmanagement_templates](variables.tf#L83) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | -| [fleet_features](variables.tf#L118) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | -| [fleet_workload_identity](variables.tf#L131) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | -| [group_iam](variables.tf#L143) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | -| [iam](variables.tf#L150) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [labels](variables.tf#L157) | Project-level labels. | map(string) | | {} | -| [nodepools](variables.tf#L163) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | -| [project_services](variables.tf#L203) | Additional project services to enable. | list(string) | | [] | +| [billing_account_id](variables.tf#L17) | Billing account ID. | string | ✓ | | +| [folder_id](variables.tf#L154) | Folder used for the GKE project in folders/nnnnnnnnnnn format. | string | ✓ | | +| [prefix](variables.tf#L205) | Prefix used for resource names. | string | ✓ | | +| [project_id](variables.tf#L214) | ID of the project that will contain all the clusters. | string | ✓ | | +| [vpc_config](variables.tf#L226) | Shared VPC project and VPC details. | object({…}) | ✓ | | +| [clusters](variables.tf#L22) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | +| [fleet_configmanagement_clusters](variables.tf#L92) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | +| [fleet_configmanagement_templates](variables.tf#L99) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | +| [fleet_features](variables.tf#L134) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | +| [fleet_workload_identity](variables.tf#L147) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | +| [group_iam](variables.tf#L159) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | +| [iam](variables.tf#L166) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [labels](variables.tf#L173) | Project-level labels. | map(string) | | {} | +| [nodepools](variables.tf#L179) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | +| [project_services](variables.tf#L219) | Additional project services to enable. | list(string) | | [] | ## Outputs diff --git a/blueprints/gke/multitenant-fleet/variables.tf b/blueprints/gke/multitenant-fleet/variables.tf index 2461ea8a..5d34440f 100644 --- a/blueprints/gke/multitenant-fleet/variables.tf +++ b/blueprints/gke/multitenant-fleet/variables.tf @@ -15,7 +15,7 @@ */ variable "billing_account_id" { - description = "Billing account id." + description = "Billing account ID." type = string } @@ -48,9 +48,25 @@ variable "clusters" { max_pods_per_node = optional(number, 110) min_master_version = optional(string) monitoring_config = optional(object({ - enable_components = optional(list(string), ["SYSTEM_COMPONENTS"]) - managed_prometheus = optional(bool) - })) + enable_system_metrics = optional(bool, true) + + # (Optional) control plane metrics + enable_api_server_metrics = optional(bool, false) + enable_controller_manager_metrics = optional(bool, false) + enable_scheduler_metrics = optional(bool, false) + + # (Optional) kube state metrics + enable_daemonset_metrics = optional(bool, false) + enable_deployment_metrics = optional(bool, false) + enable_hpa_metrics = optional(bool, false) + enable_pod_metrics = optional(bool, false) + enable_statefulset_metrics = optional(bool, false) + enable_storage_metrics = optional(bool, false) + + # Google Cloud Managed Service for Prometheus + enable_managed_prometheus = optional(bool, true) + }), {}) + node_locations = optional(list(string)) private_cluster_config = optional(any) release_channel = optional(string) diff --git a/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf +++ b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf +++ b/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/decentralized-firewall/versions.tf b/blueprints/networking/decentralized-firewall/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/decentralized-firewall/versions.tf +++ b/blueprints/networking/decentralized-firewall/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/filtering-proxy-psc/versions.tf b/blueprints/networking/filtering-proxy-psc/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/filtering-proxy-psc/versions.tf +++ b/blueprints/networking/filtering-proxy-psc/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/filtering-proxy/versions.tf b/blueprints/networking/filtering-proxy/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/filtering-proxy/versions.tf +++ b/blueprints/networking/filtering-proxy/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/hub-and-spoke-peering/versions.tf b/blueprints/networking/hub-and-spoke-peering/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/hub-and-spoke-peering/versions.tf +++ b/blueprints/networking/hub-and-spoke-peering/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/hub-and-spoke-vpn/versions.tf b/blueprints/networking/hub-and-spoke-vpn/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/hub-and-spoke-vpn/versions.tf +++ b/blueprints/networking/hub-and-spoke-vpn/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/ilb-next-hop/versions.tf b/blueprints/networking/ilb-next-hop/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/ilb-next-hop/versions.tf +++ b/blueprints/networking/ilb-next-hop/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/private-cloud-function-from-onprem/versions.tf b/blueprints/networking/private-cloud-function-from-onprem/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/private-cloud-function-from-onprem/versions.tf +++ b/blueprints/networking/private-cloud-function-from-onprem/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/networking/shared-vpc-gke/main.tf b/blueprints/networking/shared-vpc-gke/main.tf index 302ce735..88f48463 100644 --- a/blueprints/networking/shared-vpc-gke/main.tf +++ b/blueprints/networking/shared-vpc-gke/main.tf @@ -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. @@ -102,6 +102,11 @@ module "vpc-shared" { ip_cidr_range = var.ip_ranges.gce name = "gce" region = var.region + iam = { + "roles/compute.networkUser" = concat(var.owners_gce, [ + "serviceAccount:${module.project-svc-gce.service_accounts.cloud_services}", + ]) + } }, { ip_cidr_range = var.ip_ranges.gke @@ -111,24 +116,17 @@ module "vpc-shared" { pods = var.ip_secondary_ranges.gke-pods services = var.ip_secondary_ranges.gke-services } + iam = { + "roles/compute.networkUser" = concat(var.owners_gke, [ + "serviceAccount:${module.project-svc-gke.service_accounts.cloud_services}", + "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}", + ]) + "roles/compute.securityAdmin" = [ + "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}", + ] + } } ] - subnet_iam = { - "${var.region}/gce" = { - "roles/compute.networkUser" = concat(var.owners_gce, [ - "serviceAccount:${module.project-svc-gce.service_accounts.cloud_services}", - ]) - } - "${var.region}/gke" = { - "roles/compute.networkUser" = concat(var.owners_gke, [ - "serviceAccount:${module.project-svc-gke.service_accounts.cloud_services}", - "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}", - ]) - "roles/compute.securityAdmin" = [ - "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}", - ] - } - } } module "vpc-shared-firewall" { diff --git a/blueprints/networking/shared-vpc-gke/versions.tf b/blueprints/networking/shared-vpc-gke/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/networking/shared-vpc-gke/versions.tf +++ b/blueprints/networking/shared-vpc-gke/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/serverless/cloud-run-explore/README.md b/blueprints/serverless/cloud-run-explore/README.md index ff1454c1..9005165f 100644 --- a/blueprints/serverless/cloud-run-explore/README.md +++ b/blueprints/serverless/cloud-run-explore/README.md @@ -214,5 +214,5 @@ module "test" { } } -# tftest modules=4 resources=18 +# tftest modules=4 resources=19 ``` diff --git a/blueprints/third-party-solutions/README.md b/blueprints/third-party-solutions/README.md index c81bc144..62e3304e 100644 --- a/blueprints/third-party-solutions/README.md +++ b/blueprints/third-party-solutions/README.md @@ -6,12 +6,18 @@ The blueprints in this folder show how to automate installation of specific thir ### OpenShift cluster bootstrap on Shared VPC - This [example](./openshift/) shows how to quickly bootstrap an OpenShift 4.7 cluster on GCP, using typical enterprise features like Shared VPC and CMEK for instance disks. +

This [example](./openshift/) shows how to quickly bootstrap an OpenShift 4.7 cluster on GCP, using typical enterprise features like Shared VPC and CMEK for instance disks.


### Wordpress deployment on Cloud Run - This [example](./wordpress/cloudrun/) shows how to deploy a functioning new Wordpress website exposed to the public internet via CloudRun and Cloud SQL, with minimal technical overhead. +

This [example](./wordpress/cloudrun/) shows how to deploy a functioning new Wordpress website exposed to the public internet via CloudRun and Cloud SQL, with minimal technical overhead.


+ +### Serverless phpIPAM on Cloud Run + +

This [example](./phpipam/) shows how to quickly bootstrap a serverless phpIPAM instance on GCP using Cloud Run. This comes with typical enterprise features like Shared VPC, Cloud Armor with IAP and, possibly, private exposure via Internal Application Load Balancer. Indeed, the script supports deploying the application either publicly via Global Application Load Balancer with restricted access based on IPs (Cloud Armor) and identities (Identity Aware Proxy) or privately via Internal Application Load Balancer.

+ +
\ No newline at end of file diff --git a/blueprints/third-party-solutions/openshift/tf/versions.tf b/blueprints/third-party-solutions/openshift/tf/versions.tf index e4f7404f..91a91a31 100644 --- a/blueprints/third-party-solutions/openshift/tf/versions.tf +++ b/blueprints/third-party-solutions/openshift/tf/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/blueprints/third-party-solutions/phpipam/README.md b/blueprints/third-party-solutions/phpipam/README.md new file mode 100644 index 00000000..14502306 --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/README.md @@ -0,0 +1,239 @@ +# Serverless phpIPAM on Cloud Run + +[phpIPAM](https://phpipam.net/) is an open-source IP address management (IPAM) +system that can be used to manage IP addresses in both on-premises and cloud +environments. It is a powerful tool that can help businesses to automate IP +address management, proactively identify and resolve IP address conflicts, and +plan for future IP address needs. + +This repository aims to speed up deployment of phpIPAM software on Google Cloud +Platform Cloud Run serverless product. The web application can be exposed either +publicly via Global Application Load Balancer or internally via Internal +Application Load Balancer. More information on the architecture section. + +## Architecture + +![Serverless phpIPAM on Cloud Run](images/phpipam.png "Wordpress on Cloud Run") + +The main components that are deployed in this architecture are the following ( +you can learn about them by following the hyperlinks): + +- [Cloud Run](https://cloud.google.com/run): serverless PaaS offering to host + containers for web-oriented applications, while offering security, scalability + and easy versioning +- [Cloud SQL](https://cloud.google.com/sql): Managed solution for SQL databases +- [VPC Serverless Connector](https://cloud.google.com/vpc/docs/serverless-vpc-access): + Solution to access the CloudSQL VPC from Cloud Run, using only internal IP + addresses +- [Global Application Load Balancer](https://cloud.google.com/load-balancing/docs/https) (\*): + An external Application Load Balancer is a proxy-based Layer 7 load balancer + that enables you to run and scale your services behind a single external IP + address. +- [Cloud Armor](https://cloud.google.com/armor/docs/cloud-armor-overview) (\*): + Help protect your applications and websites against denial of service and web + attacks. +- [Identity Aware Proxy](https://cloud.google.com/iap/docs/concepts-overview) (\*): + IAP lets you establish a central authorization layer for applications accessed + by HTTPS, so you can use an application-level access control model instead of + relying on network-level firewalls. +- [Regional Internal Application Load Balancer](https://cloud.google.com/load-balancing/docs/l7-internal) (\*): + A Google Cloud internal Application Load Balancer is a regional proxy-based + layer 7 load balancer that enables you expose your services behind a single + internal IP address. + +> (\*) Product deployment depends on input variables + +## Setup + +### Prerequisites + +#### Setting up the project for the deployment + +This example will deploy all its resources into the project defined by +the `project_id` variable. Please note that we assume this project already +exists. However, if you provide the appropriate values to the `project_create` +variable, the project will be created as part of the deployment. + +If `project_create` is left to null, the identity performing the deployment +needs the `owner` role on the project defined by the `project_id` variable. +Otherwise, the identity performing the deployment +needs `resourcemanager.projectCreator` on the resource hierarchy node specified +by `project_create.parent` and `billing.user` on the billing account specified +by `project_create.billing_account_id`. + +### Deployment + +#### Step 0: Cloning the repository + +If you want to deploy from your Cloud Shell, click on the image below, sign in +if required and when the prompt appears, click on “confirm”. + +[![Open Cloudshell](../../../assets/images/cloud-shell-button.png)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fcloud-foundation-fabric&cloudshell_workspace=blueprints%2Fthird-party-solutions%2Fwordpress%2Fcloudrun) + +Otherwise, in your console of choice: + +```bash +git clone https://github.com/GoogleCloudPlatform/cloud-foundation-fabric +``` + +Before you deploy the architecture, you will need at least the following +information (for more precise configuration see the Variables section): + +* The project ID. + +#### Step 2: Prepare the variables + +Once you have the required information, head back to your cloned repository. +Make sure you’re in the directory of this tutorial (where this README is in). + +Configure the Terraform variables in your `terraform.tfvars` file. +See [terraform.tfvars.sample](terraform.tfvars.sample) as starting point - just +copy it to `terraform.tfvars` and edit the latter. See the variables +documentation below. + +**Notes**: + +1. If you have + the [domain restriction org. policy](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains) + on your organization, you have to edit the `cloud_run_invoker` variable and + give it a value that will be accepted in accordance to your policy. +2. By default, the application will be exposed externally through Global + Application Load Balancer, for restricting access to specific identities + please check IAP configuration or deploy the application internally via the + ILB +3. Setting the `phpipam_exposure` variable to "INTERNAL" will deploy an Internal + Application Load Balancer on the same VPC. This might be the preferred option + for enterprises since it prevents exposing the application publicly still + allowing internal access through private network (via either VPN and/or + Interconnect) + +#### Step 3: Deploy resources + +Initialize your Terraform environment and deploy the resources: + +```shell +terraform init +terraform apply +``` + +#### Step 4: Use the created resources + +Upon completion, you will see the output with the values for the Cloud Run +service and the user and password to access the application. +You can also view it later with: + +```shell +terraform output +# or for the concrete variable: +terraform output cloud_run_service +``` + +Please be aware that the password created in the script is not yet configured in the +application, you will be prompted to insert that during phpIPAM installation +process at first login. +To access the newly deployed application follow these instructions: + +1. Get the default phpIPAM url from the terraform output in the form + {IP_ADDRESS}.nip.io +2. Open your browser at that URL and you will see your phpIPAM installation page + like the following one: + +![phpIPAM Installation page](images/phpipam_install.png "phpIPAM installation page") + +3. Click on "New phpipam installation". On the next page click "Automatic + database installation", you will be prompted to the following form: + +![phpIPAM DB install](images/phpipam_db.png "phpIPAM DB installation") + +4. Insert "admin" as the MySQL username and the password available on the + terraform output of this command below (without quotes). + Untick the "Create new database" otherwise you'll get an error during + installation, leave all the other values as default and then click on " + Install phpipam database" + +``` +terraform output cloudsql_password +``` + +5. After some time a "Database installed successfully!" message should pop up. + Then click "continue" and you'll be prompted to the last form for configuring + admin credentials: + +![phpIPAM Admin setup](images/phpipam_admin.png "phpIPAM DB installation") + +6. Insert the phpipam password available in the output of the following command + and choose a site title. Then insert the site url and click "Save + settings". "A Settings updated, installation complete!" message should pop up + and clicking "Proceed to login." will redirect you to the login page. + Be aware this is just a convenient way to have a backup admin password in + terraform, you could use whatever password you prefer. + +``` +terraform output phpipam_password +``` + +7. Insert "admin" as username and the password configured on the previous step + and after login you'll finally get to the phpIPAM homepage. + +![phpIPAM Homepage](images/phpipam_home.png "phpIPAM Homepage") + +### Cleaning up your environment + +The easiest way to remove all the deployed resources is to run the following +command in Cloud Shell: + +``` {shell} +terraform destroy +``` + +The above command will delete the associated resources so there will be no +billable charges made afterwards. + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [prefix](variables.tf#L109) | Prefix used for resource names. | string | ✓ | | +| [project_id](variables.tf#L128) | Project id, references existing project if `project_create` is null. | string | ✓ | | +| [admin_principals](variables.tf#L19) | Users, groups and/or service accounts that are assigned roles, in IAM format (`group:foo@example.com`). | list(string) | | [] | +| [cloud_run_invoker](variables.tf#L25) | IAM member authorized to access the end-point (for example, 'user:YOUR_IAM_USER' for only you or 'allUsers' for everyone). | string | | "allUsers" | +| [cloudsql_password](variables.tf#L31) | CloudSQL password (will be randomly generated by default). | string | | null | +| [connector](variables.tf#L37) | Existing VPC serverless connector to use if not creating a new one. | string | | null | +| [create_connector](variables.tf#L43) | Should a VPC serverless connector be created or not. | bool | | true | +| [custom_domain](variables.tf#L49) | Cloud Run service custom domain for GLB. | string | | null | +| [iap](variables.tf#L55) | Identity-Aware Proxy for Cloud Run in the LB. | object({…}) | | {} | +| [ip_ranges](variables.tf#L67) | CIDR blocks: VPC serverless connector, Private Service Access(PSA) for CloudSQL, CloudSQL VPC. | object({…}) | | {…} | +| [phpipam_config](variables.tf#L81) | PHPIpam configuration. | object({…}) | | {…} | +| [phpipam_exposure](variables.tf#L93) | Whether to expose the application publicly via GLB or internally via ILB, default GLB. | string | | "EXTERNAL" | +| [phpipam_password](variables.tf#L103) | Password for the phpipam user (will be randomly generated by default). | string | | null | +| [project_create](variables.tf#L119) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | | null | +| [region](variables.tf#L133) | Region for the created resources. | string | | "europe-west4" | +| [security_policy](variables.tf#L139) | Security policy (Cloud Armor) to enforce in the LB. | object({…}) | | {} | +| [vpc_config](variables.tf#L149) | VPC Network and subnetwork self links for internal LB setup. | object({…}) | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [cloud_run_service](outputs.tf#L17) | CloudRun service URL. | ✓ | +| [cloudsql_password](outputs.tf#L23) | CloudSQL password. | ✓ | +| [phpipam_ip_address](outputs.tf#L29) | PHPIPAM IP Address either external or internal according to app exposure. | | +| [phpipam_password](outputs.tf#L34) | PHPIPAM user password. | ✓ | +| [phpipam_url](outputs.tf#L40) | PHPIPAM website url. | | +| [phpipam_user](outputs.tf#L45) | PHPIPAM username. | | + +## Test + +```hcl +module "test" { + source = "./fabric/blueprints/third-party-solutions/phpipam" + admin_principals = ["group:foo@example.com"] + prefix = "test" + project_create = { + billing_account_id = "1234-ABCD-1234" + parent = "folders/1234563" + } + project_id = "test-prj" +} +# tftest modules=7 resources=43 +``` diff --git a/blueprints/third-party-solutions/phpipam/cloudsql.tf b/blueprints/third-party-solutions/phpipam/cloudsql.tf new file mode 100644 index 00000000..0dc89b9a --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/cloudsql.tf @@ -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. + */ + +# Set up CloudSQL +module "cloudsql" { + source = "../../../modules/cloudsql-instance" + project_id = module.project.project_id + name = "${var.prefix}-mysql" + database_version = local.cloudsql_conf.database_version + databases = [local.cloudsql_conf.db] + network = local.network + prefix = var.prefix + region = var.region + tier = local.cloudsql_conf.tier + users = { + "${local.cloudsql_conf.user}" = var.cloudsql_password + } +} diff --git a/blueprints/third-party-solutions/phpipam/diagrams/phpipam.excalidraw b/blueprints/third-party-solutions/phpipam/diagrams/phpipam.excalidraw new file mode 100644 index 00000000..f9896973 --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/diagrams/phpipam.excalidraw @@ -0,0 +1,4821 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "image", + "version": 481, + "versionNonce": 537873588, + "isDeleted": false, + "id": "1XbyXgzt6oISJX4bJOqgJ", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1793.5245329083443, + "y": -2032.4573238238672, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "width": 87.0394357600626, + "height": 66.35855006612174, + "seed": 1031721740, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "status": "saved", + "fileId": "af2541e7127d4fdc679914759de23d8bd87e9264", + "scale": [ + 1, + 1 + ] + }, + { + "type": "line", + "version": 660, + "versionNonce": 618662028, + "isDeleted": false, + "id": "KUKHKtwx4uIJzebhF0ZW1", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1766.4112728934037, + "y": -2002.44215921633, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 49.11275214513097, + "height": 36.92648039990984, + "seed": 292309772, + "groupIds": [ + "143QJr2AU36qqThhRKQql", + "Nr52ogYxUnYvl_fIZutZ_" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 12.265055829310315, + 36.92648039990984 + ], + [ + 49.11275214513097, + 36.92648039990984 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 660, + "versionNonce": 527144500, + "isDeleted": false, + "id": "03GsUk9b3uMGDNQosA5W_", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1815.5240250385348, + "y": -1965.5156788164204, + "strokeColor": "#000000", + "backgroundColor": "#4285f4", + "width": 49.11275214513097, + "height": 36.92648673988745, + "seed": 1447496076, + "groupIds": [ + "143QJr2AU36qqThhRKQql", + "Nr52ogYxUnYvl_fIZutZ_" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -36.847696315820656, + 0 + ], + [ + -49.11275214513097, + 36.92648673988745 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 661, + "versionNonce": 258414348, + "isDeleted": false, + "id": "wzCWvE55EqWjCYcux-V0-", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1744.875204393739, + "y": -1928.5891920765328, + "strokeColor": "#000000", + "backgroundColor": "#4285f4", + "width": 21.536068499663806, + "height": 36.92648673988745, + "seed": 1581643788, + "groupIds": [ + "143QJr2AU36qqThhRKQql", + "Nr52ogYxUnYvl_fIZutZ_" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 12.317583248316135, + -6.145664348279347 + ], + [ + 21.536068499663806, + -36.92648673988745 + ], + [ + 9.21848947799943, + -36.92648673988745 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 660, + "versionNonce": 1333964724, + "isDeleted": false, + "id": "AmWl3EKGBEqQ6c-2l5fLr", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1744.875204393739, + "y": -2002.44215921633, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 21.536068499663806, + "height": 36.92648039990984, + "seed": 358815372, + "groupIds": [ + "143QJr2AU36qqThhRKQql", + "Nr52ogYxUnYvl_fIZutZ_" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 9.21848947799943, + 36.92648039990984 + ], + [ + 21.536068499663806, + 36.92648039990984 + ], + [ + 12.317583248316135, + 6.145659417185632 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 749, + "versionNonce": 1945012, + "isDeleted": false, + "id": "IfVzFaOMx3j3dME8GrhUs", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1743.866131613274, + "y": -1927.1534159338844, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 68.65559387207031, + "height": 16.7247155341873, + "seed": 1482188044, + "groupIds": [ + "Nr52ogYxUnYvl_fIZutZ_" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "1hR9SCYu3Fd8-OWyibUBe", + "type": "arrow" + } + ], + "updated": 1693311484476, + "link": null, + "locked": false, + "fontSize": 14.539679438756231, + "fontFamily": 2, + "text": "Cloud Run", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Cloud Run", + "lineHeight": 1.150280898876405, + "baseline": 13 + }, + { + "type": "rectangle", + "version": 528, + "versionNonce": 1009242420, + "isDeleted": false, + "id": "c9Cux6NrH-jflel7CZ993", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1491.6056084907252, + "y": -2130.145262517028, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 701.1157994906367, + "height": 539.060730392323, + "seed": 304311604, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1693311392567, + "link": null, + "locked": false + }, + { + "type": "image", + "version": 709, + "versionNonce": 519142028, + "isDeleted": false, + "id": "YYw7gXl4W97O0JXZAMzhR", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1472.3435624427782, + "y": -2215.1254824711386, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "width": 372.22474653284047, + "height": 248.14983102189362, + "seed": 505448500, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311401662, + "link": null, + "locked": false, + "status": "saved", + "fileId": "7f10a90d0c745f95f1922694e27ad51a6bf7d09e", + "scale": [ + 1, + 1 + ] + }, + { + "type": "rectangle", + "version": 672, + "versionNonce": 1565720756, + "isDeleted": false, + "id": "hWzo_PR1wR4eOgN7GCA76", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1532.7305950497039, + "y": -1813.6835962459613, + "strokeColor": "#000000", + "backgroundColor": "#a5d8ff", + "width": 603.3558049160198, + "height": 204.09209589366222, + "seed": 168241844, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1693311318058, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 566, + "versionNonce": 1441061940, + "isDeleted": false, + "id": "YlvLcdA67ovtgbCNjDHKi", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1737.185641562936, + "y": -1701.0911930806487, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 30.93220427159039, + "height": 29.15983311685192, + "seed": 2145151540, + "groupIds": [ + "4QQEDIAcbGDabZcoMrnE9", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 12.702040814491287 + ], + [ + 30.93220427159039, + 29.15983311685192 + ], + [ + 30.93220427159039, + 16.457792302360613 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 566, + "versionNonce": 210579724, + "isDeleted": false, + "id": "AJGjPZwDhTGk5R9odCXcm", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1737.185641562936, + "y": -1681.4684411845014, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 30.93220427159039, + "height": 29.159827080170565, + "seed": 314114996, + "groupIds": [ + "-mwo8PSPLEJROjucGMNV9", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 12.702040814491289 + ], + [ + 30.93220427159039, + 29.159827080170565 + ], + [ + 30.93220427159039, + 16.45778626567926 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 568, + "versionNonce": 853751220, + "isDeleted": false, + "id": "3JOs7lBPu2ZLdLKA7z_sc", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1768.1178458345266, + "y": -1671.9313599637967, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 30.932208799101403, + "height": 29.15983311685192, + "seed": 566652212, + "groupIds": [ + "Ssfz8of57AsqfvU9c6tHU", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 30.932208799101403, + -16.45779230236061 + ], + [ + 30.932208799101403, + -29.15983311685192 + ], + [ + 0, + -12.702040814491282 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 568, + "versionNonce": 1213935500, + "isDeleted": false, + "id": "EkV4IW-AJzsz-vh97G-3_", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1768.1178458345266, + "y": -1652.3086141043307, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 30.932208799101403, + "height": 29.159827080170565, + "seed": 241870516, + "groupIds": [ + "9FDnYXoLyfWDYgY1D52rq", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 30.932208799101403, + -16.457786265679268 + ], + [ + 30.932208799101403, + -29.159827080170565 + ], + [ + 0, + -12.702040814491303 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 568, + "versionNonce": 1641701172, + "isDeleted": false, + "id": "NM4FCQHCF6jXUQseMSki-", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1799.050054633628, + "y": -1707.5477105312384, + "strokeColor": "#000000", + "backgroundColor": "#4285f4", + "width": 30.932208799101403, + "height": 29.159827834755752, + "seed": 491499572, + "groupIds": [ + "z8fP_4PYXZfW0WAcJfUPu", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -12.702037796150632 + ], + [ + -30.932208799101403, + -29.159827834755752 + ], + [ + -30.932208799101403, + -16.457789284019967 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 569, + "versionNonce": 115695116, + "isDeleted": false, + "id": "i8zbbwYqriRe6mw4nCYgK", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1768.1178458345264, + "y": -1736.7075383659944, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 30.93220427159039, + "height": 29.159827834755752, + "seed": 240093620, + "groupIds": [ + "1_yORLPOgADxV6GB4-GxX", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -30.93220427159039, + 16.457790038605133 + ], + [ + -30.93220427159039, + 29.159827834755752 + ], + [ + 0, + 12.702038550735798 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 566, + "versionNonce": 1261459636, + "isDeleted": false, + "id": "nLcf6dXvi-Dg0zFYxD3my", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1737.185641562936, + "y": -1720.376346590407, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 30.93220427159039, + "height": 29.20202650117651, + "seed": 882651956, + "groupIds": [ + "9KxAiBauJwpvFBEwLOUoa", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 12.74424023549724 + ], + [ + 30.93220427159039, + 29.20202650117651 + ], + [ + 30.93220427159039, + 16.457792302360623 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 569, + "versionNonce": 19250316, + "isDeleted": false, + "id": "h6Z26XLvWgyFchxTQvpXL", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1768.1178458345266, + "y": -1691.1743200892304, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 30.932208799101403, + "height": 29.20202650117651, + "seed": 244821172, + "groupIds": [ + "-h4pDIR_NFTn_DviMiwQz", + "aDHYiPoTbYMWlla5qLL3x", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 30.932208799101403, + -16.457786265679268 + ], + [ + 30.932208799101403, + -29.20202650117651 + ], + [ + 0, + -12.744234198815874 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 832, + "versionNonce": 1519067700, + "isDeleted": false, + "id": "EgR1hwDJN0PjLrGd5nbjr", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1732.6497236445011, + "y": -1650.0718103364015, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 71.41729736328125, + "height": 16.969289811898413, + "seed": 1575482932, + "groupIds": [ + "10xnq85pGHDASfiSQPfuC", + "xRkpIu4RUHYZful5rB5uJ", + "m17ghiEvwrmuZDB7XkBzD", + "sRnoZ2rKaS2Y28Pz_3wfo" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "fontSize": 14.604231998393391, + "fontFamily": 2, + "text": "Cloud SQL", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Cloud SQL", + "lineHeight": 1.161943319838058, + "baseline": 13 + }, + { + "type": "text", + "version": 11, + "versionNonce": 1087785740, + "isDeleted": false, + "id": "8TOTAlez0vKnEmM_OajUD", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1604.7981078973241, + "y": -1786.4960135936278, + "strokeColor": "#495057", + "backgroundColor": "#a5d8ff", + "width": 57.572265625, + "height": 32.199999999999996, + "seed": 631585716, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 2, + "text": "VPC", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "VPC", + "lineHeight": 1.15, + "baseline": 26 + }, + { + "type": "rectangle", + "version": 664, + "versionNonce": 701096844, + "isDeleted": false, + "id": "WZfd3JxWdQGj02Nx1ywUK", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1757.5152703538752, + "y": -1843.3509692696189, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 30.71336203849055, + "height": 5.031485400485886, + "seed": 374877708, + "groupIds": [ + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 689, + "versionNonce": 450947892, + "isDeleted": false, + "id": "C58IGor8Vlna-k9T4SG_E", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1758.8846917733617, + "y": -1801.2793496901554, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 30.71336203849055, + "height": 5.031485400485886, + "seed": 968789132, + "groupIds": [ + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 715, + "versionNonce": 811438604, + "isDeleted": false, + "id": "2tGkxza5W0dNG2l7A52cu", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 1.5707963267948957, + "x": 1778.548046982738, + "y": -1821.9384827513968, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 30.71336203849055, + "height": 5.031485400485886, + "seed": 232069900, + "groupIds": [ + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 724, + "versionNonce": 998597812, + "isDeleted": false, + "id": "jfdivAKJ-QRP9KZrYLtRZ", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 1.5707963267948957, + "x": 1736.2414295015294, + "y": -1820.9136694039341, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 30.71336203849055, + "height": 5.031485400485886, + "seed": 1830148492, + "groupIds": [ + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 964, + "versionNonce": 897020044, + "isDeleted": false, + "id": "qWQrT53vm2Vegs-_kUpu5", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1742.5563882472707, + "y": -1849.643844484846, + "strokeColor": "#669df6", + "backgroundColor": "#aecbfa", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 1387972620, + "groupIds": [ + "6TZ5hG1Ee6MddXwuJXdr5", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1006, + "versionNonce": 341749300, + "isDeleted": false, + "id": "e3s68qp5MX_7Ayqq5sf77", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1751.7514222120528, + "y": -1849.7837438562246, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 1119741580, + "groupIds": [ + "6TZ5hG1Ee6MddXwuJXdr5", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 940, + "versionNonce": 1758560012, + "isDeleted": false, + "id": "B8bLOHZrRhslEVbBP9jp4", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1784.9536781277313, + "y": -1850.050828266343, + "strokeColor": "#669df6", + "backgroundColor": "#aecbfa", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 836318476, + "groupIds": [ + "6DOjq1TwR59SoS2HlstbS", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 982, + "versionNonce": 1716701108, + "isDeleted": false, + "id": "DWq3ahV34UH2RP3JIa7EI", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1794.1487120925142, + "y": -1850.1907276377208, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 1837544332, + "groupIds": [ + "6DOjq1TwR59SoS2HlstbS", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 940, + "versionNonce": 1927658892, + "isDeleted": false, + "id": "qDYX7fV1q3K5MDGAqCMtJ", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1785.4598412517405, + "y": -1806.9553369965429, + "strokeColor": "#669df6", + "backgroundColor": "#aecbfa", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 67133964, + "groupIds": [ + "hdInUI-orAUWqQtInirJI", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 982, + "versionNonce": 1703391540, + "isDeleted": false, + "id": "SeTrg7R5VpHNGmyPOfW-J", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1794.6548752165222, + "y": -1807.0952363679203, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 742408332, + "groupIds": [ + "hdInUI-orAUWqQtInirJI", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 944, + "versionNonce": 1559112716, + "isDeleted": false, + "id": "cYzBD-ipbA2Cd4ciFIXPC", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1742.5257427528738, + "y": -1807.5070327656533, + "strokeColor": "#669df6", + "backgroundColor": "#aecbfa", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 771772172, + "groupIds": [ + "OYW2zQVmsyxVaA_wYND5b", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 986, + "versionNonce": 1365469876, + "isDeleted": false, + "id": "Zi6tOTdx4Ut6ljQvl5B9v", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1751.7207767176571, + "y": -1807.6469321370298, + "strokeColor": "#1864ab", + "backgroundColor": "#669df6", + "width": 8.85797007441361, + "height": 17.70860035786427, + "seed": 1236962700, + "groupIds": [ + "OYW2zQVmsyxVaA_wYND5b", + "xe_VmpQuCkK-2Lu9CGL8R" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 659, + "versionNonce": 2028259980, + "isDeleted": false, + "id": "5ggsny_XDGStwZ4geHTfV", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2028.395096217105, + "y": -1979.2189212533528, + "strokeColor": "#000000", + "backgroundColor": "#4285f4", + "width": 7.100055542062618, + "height": 14.200096227246, + "seed": 1263001868, + "groupIds": [ + "aKhaUT8I7Mv1hKXBftWpR", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -7.100055542062618, + 0 + ], + [ + -7.100055542062618, + -14.200096227246 + ], + [ + 0, + -14.200096227246 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 210498612, + "isDeleted": false, + "id": "FoSfSZMYBdt05snHqeZzA", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1996.444869801216, + "y": -1979.2189212533528, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 56.80039233742361, + "height": 17.750125236350584, + "seed": 1010818956, + "groupIds": [ + "8t5lPH-tVD4D3JjRJY2Da", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 17.750125236350584 + ], + [ + 7.100049351696269, + 17.750125236350584 + ], + [ + 7.100049351696269, + 7.10005554206262 + ], + [ + 24.850170873827043, + 7.10005554206262 + ], + [ + 24.850170873827043, + 17.750125236350584 + ], + [ + 31.950226415889656, + 17.750125236350584 + ], + [ + 31.950226415889656, + 7.10005554206262 + ], + [ + 49.70034917609371, + 7.10005554206262 + ], + [ + 49.70034917609371, + 17.750125236350584 + ], + [ + 56.80039233742361, + 17.750125236350584 + ], + [ + 56.80039233742361, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 1233361164, + "isDeleted": false, + "id": "ypWdYRNc4UGk91mI9W6qc", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2003.544919152912, + "y": -2011.1691427169494, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 42.60029982439741, + "height": 17.750125236350584, + "seed": 2058620428, + "groupIds": [ + "iivjceHzn0JoLGUUUq30p", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 42.60029982439741, + 0 + ], + [ + 42.60029982439741, + 17.750125236350584 + ], + [ + 0, + 17.750125236350584 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 887573940, + "isDeleted": false, + "id": "S0OfeeLQomDTwrfbWdVPW", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2024.8450672080003, + "y": -2011.1691427169494, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 21.30015176930862, + "height": 17.750125236350584, + "seed": 1738435724, + "groupIds": [ + "gxQeb-Dyeh10hmcLocvwG", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.30015176930862, + 0 + ], + [ + 21.30015176930862, + 17.750125236350584 + ], + [ + 0, + 17.750125236350584 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 1782880140, + "isDeleted": false, + "id": "msPcz2lJcuW8pI52B7no2", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2039.045165911394, + "y": -1961.4687960170022, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 21.300149293162075, + "height": 21.300149293162075, + "seed": 884538124, + "groupIds": [ + "OotFB7H6XWAQ1ekdcaEQw", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.300149293162075, + 0 + ], + [ + 21.300149293162075, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 1000044340, + "isDeleted": false, + "id": "46ogLanYL-b5Vshd1BuQ_", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1989.3448192114458, + "y": -1961.4687960170022, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 21.300149293162075, + "height": 21.300149293162075, + "seed": 1283307916, + "groupIds": [ + "OotFB7H6XWAQ1ekdcaEQw", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.300149293162075, + 0 + ], + [ + 21.300149293162075, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 2002896396, + "isDeleted": false, + "id": "jOvslEebqD-I0qJnprYsh", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1999.9948950961002, + "y": -1961.4687960170022, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 10.650073408507769, + "height": 21.300149293162075, + "seed": 975611916, + "groupIds": [ + "6wBq97BQG5vcRxZ7GY2Li", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 10.650073408507769, + 0 + ], + [ + 10.650073408507769, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 2098098356, + "isDeleted": false, + "id": "aNxix0zmn9vAvQeeYHjXp", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2014.1949925614203, + "y": -1961.4687960170022, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 21.30015176930862, + "height": 21.300149293162075, + "seed": 296601228, + "groupIds": [ + "PmWrgGGmRyNNh7W_7SpcI", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.30015176930862, + 0 + ], + [ + 21.30015176930862, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 1976902796, + "isDeleted": false, + "id": "-gCeUl23RAdBaqghoMu75", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2024.8450672080003, + "y": -1961.4687960170022, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 10.650077122727579, + "height": 21.300149293162075, + "seed": 1473058060, + "groupIds": [ + "KZ7foUUozdX0JleP4ZFjN", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 10.650077122727579, + 0 + ], + [ + 10.650077122727579, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 658, + "versionNonce": 1491483188, + "isDeleted": false, + "id": "UuIl_4Ro0nQeczSgTWIqF", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2049.695245510268, + "y": -1961.4687960170022, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 10.650069694287957, + "height": 21.300149293162075, + "seed": 1778291596, + "groupIds": [ + "KZ7foUUozdX0JleP4ZFjN", + "s71cWJFwFITMU-Lps6d9w", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 10.650069694287957, + 0 + ], + [ + 10.650069694287957, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 1380, + "versionNonce": 995668748, + "isDeleted": false, + "id": "CyL4xr_Q5hzWp5O3epy6O", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1975.1627292376666, + "y": -1937.3655267831248, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 99.56015014648438, + "height": 27.44457849673917, + "seed": 1719419404, + "groupIds": [ + "vBFiVSGUjnxeTQibkcWbp", + "2Xg7V_RgexkPamV5bhliA", + "Jy4_A0VUJJS52Onc5RubG", + "keZ69FOJuasvauJpOdukW", + "1whHlGRkzVeXj0v3-syM0", + "GwTn6jd8QGpnJKyf_0Ggi", + "_AHsh0V9g7Gmkyg8iVyyC" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "fontSize": 11.942915477701884, + "fontFamily": 2, + "text": "Global Application \nLoad Balancer", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Global Application \nLoad Balancer", + "lineHeight": 1.1489898989898986, + "baseline": 24 + }, + { + "type": "rectangle", + "version": 571, + "versionNonce": 1239784204, + "isDeleted": false, + "id": "vGubQl3uFUNUf5EgTMZxr", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2460.4412056033975, + "y": -1998.999805226251, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "width": 58.14103866046347, + "height": 38.76069244030904, + "seed": 484798004, + "groupIds": [ + "AgHaNJVHW2KnAgQ7rVvSW" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311622859, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 682, + "versionNonce": 1696492468, + "isDeleted": false, + "id": "UboAAINFFvnut6GEQ5Qf0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2454.8958967490294, + "y": -1955.924839909427, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "width": 68.31684981684984, + "height": 9.351355868465966, + "seed": 805406644, + "groupIds": [ + "AgHaNJVHW2KnAgQ7rVvSW" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311622859, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 851, + "versionNonce": 1393973644, + "isDeleted": false, + "id": "fgmq6SWA3TxH_OK31cxRI", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2505.263704355122, + "y": -1952.500399732242, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 12.241641915449078, + "height": 2.3597905067140177, + "seed": 190738740, + "groupIds": [ + "AgHaNJVHW2KnAgQ7rVvSW" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311622859, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 953, + "versionNonce": 1396626740, + "isDeleted": false, + "id": "zfNuhr69KIBjKjVv3Te23", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2483.575583232952, + "y": -1959.6346501013777, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 12.241641915449078, + "height": 2.3597905067140177, + "seed": 241314484, + "groupIds": [ + "AgHaNJVHW2KnAgQ7rVvSW" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311622859, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1280, + "versionNonce": 615204876, + "isDeleted": false, + "id": "m4iruD5GCZYHmsh6ARTfN", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2465.434373252083, + "y": -1994.5155791557954, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 48.22892577732466, + "height": 30.250725686721108, + "seed": 1001567284, + "groupIds": [ + "AgHaNJVHW2KnAgQ7rVvSW" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [ + { + "id": "TAuiOhdXL8nMhW4lSzGVF", + "type": "arrow" + } + ], + "updated": 1693311622859, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 393, + "versionNonce": 1150684852, + "isDeleted": false, + "id": "TAuiOhdXL8nMhW4lSzGVF", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2449.6334814011047, + "y": -1973.621191427633, + "strokeColor": "#495057", + "backgroundColor": "#a5d8ff", + "width": 367.16286324641396, + "height": 1.267797616311782, + "seed": 540427276, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311622859, + "link": null, + "locked": false, + "startBinding": { + "elementId": "m4iruD5GCZYHmsh6ARTfN", + "focus": -0.37026341157566467, + "gap": 15.800891850978132 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -367.16286324641396, + 1.267797616311782 + ] + ] + }, + { + "type": "arrow", + "version": 619, + "versionNonce": 545017524, + "isDeleted": false, + "id": "jld-qaYp-lDgq1Hm1GEN6", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2449.642702346363, + "y": -1700.1766895233552, + "strokeColor": "#495057", + "backgroundColor": "#a5d8ff", + "width": 368.65484385066657, + "height": 0.699874537994674, + "seed": 992116916, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311370548, + "link": null, + "locked": false, + "startBinding": { + "elementId": "oDtXSp8ctOQP7XMgXUvae", + "focus": 4.148172941424359, + "gap": 14.90177671033041 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -368.65484385066657, + 0.699874537994674 + ] + ] + }, + { + "type": "line", + "version": 1061, + "versionNonce": 349228300, + "isDeleted": false, + "id": "R5B-Lf8AWDyW_GkYfTlqF", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2027.5186511344352, + "y": -1712.8244280181282, + "strokeColor": "#000000", + "backgroundColor": "#4285f4", + "width": 7.100055542062618, + "height": 14.200096227246, + "seed": 851713204, + "groupIds": [ + "CQ1bM3lzktqbLiEs8kcm9", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -7.100055542062618, + 0 + ], + [ + -7.100055542062618, + -14.200096227246 + ], + [ + 0, + -14.200096227246 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 2000116148, + "isDeleted": false, + "id": "75HIMaRCT06X2FxFa6xIo", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1995.568424718546, + "y": -1712.8244280181282, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 56.80039233742361, + "height": 17.750125236350584, + "seed": 587621940, + "groupIds": [ + "YbcJIKMp3pnjXFjw5gmjG", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 17.750125236350584 + ], + [ + 7.100049351696269, + 17.750125236350584 + ], + [ + 7.100049351696269, + 7.10005554206262 + ], + [ + 24.850170873827043, + 7.10005554206262 + ], + [ + 24.850170873827043, + 17.750125236350584 + ], + [ + 31.950226415889656, + 17.750125236350584 + ], + [ + 31.950226415889656, + 7.10005554206262 + ], + [ + 49.70034917609371, + 7.10005554206262 + ], + [ + 49.70034917609371, + 17.750125236350584 + ], + [ + 56.80039233742361, + 17.750125236350584 + ], + [ + 56.80039233742361, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 1036733324, + "isDeleted": false, + "id": "iygoDAQ2e2pWIXqjFLgtr", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2002.668474070242, + "y": -1744.774649481725, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 42.60029982439741, + "height": 17.750125236350584, + "seed": 927170484, + "groupIds": [ + "oTK-E8aRZNeeuwG7ncCQb", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 42.60029982439741, + 0 + ], + [ + 42.60029982439741, + 17.750125236350584 + ], + [ + 0, + 17.750125236350584 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 1342833460, + "isDeleted": false, + "id": "STTal_nPnwwcdnULvH1s_", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2023.9686221253303, + "y": -1744.774649481725, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 21.30015176930862, + "height": 17.750125236350584, + "seed": 1201760564, + "groupIds": [ + "gCinajIwnuUWSBHE-sQcZ", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.30015176930862, + 0 + ], + [ + 21.30015176930862, + 17.750125236350584 + ], + [ + 0, + 17.750125236350584 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 267516428, + "isDeleted": false, + "id": "dxF-hE2_N_vU2cU-pBAxQ", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2038.1687208287235, + "y": -1695.0743027817778, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 21.300149293162075, + "height": 21.300149293162075, + "seed": 931537588, + "groupIds": [ + "Gh2KqpIV1BDMOrlVjRw0t", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.300149293162075, + 0 + ], + [ + 21.300149293162075, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 466320564, + "isDeleted": false, + "id": "g5zqGfYvXPKxPuBkOysUV", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1988.468374128776, + "y": -1695.0743027817778, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 21.300149293162075, + "height": 21.300149293162075, + "seed": 228043828, + "groupIds": [ + "Gh2KqpIV1BDMOrlVjRw0t", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.300149293162075, + 0 + ], + [ + 21.300149293162075, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 1983830156, + "isDeleted": false, + "id": "2uQ9yIYgEOGIY0aMPaISL", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1999.1184500134302, + "y": -1695.0743027817778, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 10.650073408507769, + "height": 21.300149293162075, + "seed": 196871604, + "groupIds": [ + "WYbK7bfNXbkEfUGJfZMQq", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 10.650073408507769, + 0 + ], + [ + 10.650073408507769, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 874628660, + "isDeleted": false, + "id": "hTgnL6v7qkAPTso2cg7Cc", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2013.3185474787504, + "y": -1695.0743027817778, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 21.30015176930862, + "height": 21.300149293162075, + "seed": 1192832820, + "groupIds": [ + "Hu7ZxdfF0OFO-s4CEPgpT", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.30015176930862, + 0 + ], + [ + 21.30015176930862, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 1819010828, + "isDeleted": false, + "id": "K3s1Vnsu7Ig84d8WlyZLN", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2023.9686221253303, + "y": -1695.0743027817778, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 10.650077122727579, + "height": 21.300149293162075, + "seed": 1075128500, + "groupIds": [ + "DtinCYcF5v_IxZzOSB7KJ", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 10.650077122727579, + 0 + ], + [ + 10.650077122727579, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1060, + "versionNonce": 1307762612, + "isDeleted": false, + "id": "ddCzKIj9MKTWcEJNDkTPP", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2048.8188004275976, + "y": -1695.0743027817778, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 10.650069694287957, + "height": 21.300149293162075, + "seed": 2052452916, + "groupIds": [ + "DtinCYcF5v_IxZzOSB7KJ", + "GBOtEdRzL8dZfz5OAKnkX", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 10.650069694287957, + 0 + ], + [ + 10.650069694287957, + 21.300149293162075 + ], + [ + 0, + 21.300149293162075 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 1790, + "versionNonce": 1727210892, + "isDeleted": false, + "id": "NdTyUMyVg94zhiQoU_aYH", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1971.6306902829263, + "y": -1670.9710335479006, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 104.871337890625, + "height": 27.44457849673917, + "seed": 348228532, + "groupIds": [ + "41ylW99tmvbdcONHuTjBJ", + "6SNEKlxIFFvifHmQdQfYo", + "l_CRgPkxtok1To7KswRaa", + "DRfmOGZAtVLdORn2xR7yn", + "itc1FZ0Xs2C1kbMujZH3W", + "poY-lWcY20jfnarj8L3hI", + "0mZoIb0G3Op6Tv2Ek2tUm" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311295407, + "link": null, + "locked": false, + "fontSize": 11.942915477701884, + "fontFamily": 2, + "text": "Internal Application \nLoad Balancer", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Internal Application \nLoad Balancer", + "lineHeight": 1.1489898989898986, + "baseline": 24 + }, + { + "type": "line", + "version": 311, + "versionNonce": 754670260, + "isDeleted": false, + "id": "il4-XEVmby3tUIrvWychm", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2135.7533145705465, + "y": -1723.4518645312019, + "strokeColor": "#495057", + "backgroundColor": "#a5d8ff", + "width": 229.85181919907745, + "height": 1.5896375165302743, + "seed": 1306986508, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311374017, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 229.85181919907745, + 1.5896375165302743 + ] + ] + }, + { + "type": "rectangle", + "version": 820, + "versionNonce": 1107363852, + "isDeleted": false, + "id": "6D4oJaBAuLoDYS0zF_CWc", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2366.761316286691, + "y": -1810.3894047977235, + "strokeColor": "#000000", + "backgroundColor": "#e9ecef", + "width": 267.30751899828243, + "height": 204.09209589366222, + "seed": 1790217356, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1693311370548, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 106, + "versionNonce": 999402420, + "isDeleted": false, + "id": "aOb2ctCdAG_zpbjcLInk7", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2386.830190811656, + "y": -1791.7940740188417, + "strokeColor": "#495057", + "backgroundColor": "#a5d8ff", + "width": 113.572265625, + "height": 32.199999999999996, + "seed": 5710604, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311325062, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 2, + "text": "On-Prem", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "On-Prem", + "lineHeight": 1.15, + "baseline": 26 + }, + { + "type": "rectangle", + "version": 498, + "versionNonce": 651282572, + "isDeleted": false, + "id": "8prp-Y-70ERWfgc7aC2Go", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2466.891987197782, + "y": -1728.3498781298488, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "width": 58.14103866046347, + "height": 38.76069244030904, + "seed": 1368666548, + "groupIds": [ + "fva9Mw0UhifIbppAvrWKB" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311341279, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 610, + "versionNonce": 852047372, + "isDeleted": false, + "id": "oDtXSp8ctOQP7XMgXUvae", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2461.3466783434137, + "y": -1685.2749128130247, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "width": 68.31684981684984, + "height": 9.351355868465966, + "seed": 2030820148, + "groupIds": [ + "fva9Mw0UhifIbppAvrWKB" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [ + { + "id": "jld-qaYp-lDgq1Hm1GEN6", + "type": "arrow" + } + ], + "updated": 1693311367412, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 778, + "versionNonce": 1830868748, + "isDeleted": false, + "id": "K5WdDbm7qlHVQ9sg0unq-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2511.714485949507, + "y": -1681.8504726358399, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 12.241641915449078, + "height": 2.3597905067140177, + "seed": 1993495732, + "groupIds": [ + "fva9Mw0UhifIbppAvrWKB" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311341279, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 880, + "versionNonce": 323284916, + "isDeleted": false, + "id": "itEeNm3te2cPocf6QqiJM", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2490.0263648273362, + "y": -1688.9847230049756, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 12.241641915449078, + "height": 2.3597905067140177, + "seed": 948805172, + "groupIds": [ + "fva9Mw0UhifIbppAvrWKB" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311341279, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1207, + "versionNonce": 1669993868, + "isDeleted": false, + "id": "qehCG6gKwZ0RnSR7ipnqd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2471.8851548464677, + "y": -1723.8656520593934, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 48.22892577732466, + "height": 30.250725686721108, + "seed": 119781300, + "groupIds": [ + "fva9Mw0UhifIbppAvrWKB" + ], + "frameId": null, + "roundness": { + "type": 1 + }, + "boundElements": [], + "updated": 1693311341279, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 33, + "versionNonce": 1139228212, + "isDeleted": false, + "id": "xCVELrM_0z2Bt7m7t6pMl", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2197.826319894205, + "y": -1758.9976717266175, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "width": 156.748046875, + "height": 23, + "seed": 262530100, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311490560, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 2, + "text": "VPN/Interconnect", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "VPN/Interconnect", + "lineHeight": 1.15, + "baseline": 19 + }, + { + "type": "line", + "version": 187, + "versionNonce": 1239730612, + "isDeleted": false, + "id": "n_lhRcvZtm3yWi3zNEBJV", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1964.9459496117454, + "y": -1958.8875906849437, + "strokeColor": "#1971c2", + "backgroundColor": "#e9ecef", + "width": 126.17815604760767, + "height": 1.1773083313553343, + "seed": 1396605748, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311460507, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -126.17815604760767, + 1.1773083313553343 + ] + ] + }, + { + "type": "line", + "version": 306, + "versionNonce": 1257036684, + "isDeleted": false, + "id": "4L4IgDappf0lRmyc8X10d", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1961.783334454876, + "y": -1710.9000432513847, + "strokeColor": "#1971c2", + "backgroundColor": "#e9ecef", + "width": 122.06028957987155, + "height": 243.10060696483674, + "seed": 913199116, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311460507, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -122.06028957987155, + -243.10060696483674 + ] + ] + }, + { + "type": "arrow", + "version": 208, + "versionNonce": 2022298036, + "isDeleted": false, + "id": "1hR9SCYu3Fd8-OWyibUBe", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1772.833101624224, + "y": -1902.1597754192694, + "strokeColor": "#1971c2", + "backgroundColor": "#e9ecef", + "width": 0.6456206978398313, + "height": 155.9743650605269, + "seed": 69677324, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693311488870, + "link": null, + "locked": false, + "startBinding": { + "elementId": "IfVzFaOMx3j3dME8GrhUs", + "focus": 0.15801185672017465, + "gap": 8.268924980427641 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0.6456206978398313, + 155.9743650605269 + ] + ] + }, + { + "type": "text", + "version": 304, + "versionNonce": 594979252, + "isDeleted": false, + "id": "n7ln0pmmtpM6jtsJAZLmo", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1617.267374474415, + "y": -1860.5351407849744, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "width": 117.3671875, + "height": 36.8, + "seed": 910776244, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311514550, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 2, + "text": "VPC Serverless \nConnector", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "VPC Serverless \nConnector", + "lineHeight": 1.15, + "baseline": 33 + }, + { + "type": "line", + "version": 337, + "versionNonce": 1336321716, + "isDeleted": false, + "id": "R-U4aOs1dKPpH1p-kUXVj", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2194.3454487369822, + "y": -2008.1902074796785, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 53.252513658530155, + "height": 66.25938635522165, + "seed": 1866338828, + "groupIds": [ + "ng1U6l9sAI234KMxvTZ2o", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 26.626256829265078, + 11.78184860439602 + ], + [ + 26.626256829265078, + 29.652788806337103 + ], + [ + 26.626256829265078, + 29.652788806337103 + ], + [ + 26.604619250085825, + 31.214368306506216 + ], + [ + 26.52013407235635, + 32.76351547468527 + ], + [ + 26.373875363635207, + 34.29878764285813 + ], + [ + 26.166873252353522, + 35.818725055570226 + ], + [ + 25.900201806069838, + 37.32187772161756 + ], + [ + 25.574910681716354, + 38.806795649796086 + ], + [ + 25.192054418350562, + 40.272023966776544 + ], + [ + 24.752692437155186, + 41.71612244560541 + ], + [ + 24.257874395062434, + 43.13762644870285 + ], + [ + 23.708659713255045, + 44.535090866990146 + ], + [ + 23.10610781291576, + 45.90706570926324 + ], + [ + 22.451263468851515, + 47.252091220067584 + ], + [ + 21.745200748620857, + 48.56872717244969 + ], + [ + 20.98894978065492, + 49.855513810955 + ], + [ + 20.183584632512247, + 51.11100602650476 + ], + [ + 19.330159843250307, + 52.33374894576966 + ], + [ + 18.4297250698013, + 53.52228769542044 + ], + [ + 17.48333485122271, + 54.675177166378305 + ], + [ + 16.492048608697264, + 55.790962485313976 + ], + [ + 15.456920881282457, + 56.868193661023454 + ], + [ + 14.379006208035735, + 57.9054158201774 + ], + [ + 13.259368892265126, + 58.90118385369711 + ], + [ + 12.09904882665229, + 59.85404288825326 + ], + [ + 10.89911031450523, + 60.76254293264181 + ], + [ + 9.66060789488143, + 61.6252291135335 + ], + [ + 8.384600988963609, + 62.44065143972429 + ], + [ + 7.072144135809265, + 63.20735992001017 + ], + [ + 5.724286992350592, + 63.9239045631871 + ], + [ + 4.342088979770334, + 64.58883049592579 + ], + [ + 2.926599755000689, + 65.20068772702223 + ], + [ + 1.4788885034749268, + 65.7580213831471 + ], + [ + 0, + 66.25938635522165 + ], + [ + -1.477797348477959, + 65.7591882110857 + ], + [ + -2.9244906768857386, + 65.20307508627732 + ], + [ + -4.339025446165864, + 64.59250185412587 + ], + [ + -5.720344676198234, + 63.92892338796062 + ], + [ + -7.067391386862741, + 63.21377991473506 + ], + [ + -8.379113480164545, + 62.44851654352796 + ], + [ + -9.654453975983534, + 61.63458326554331 + ], + [ + -10.892353453136977, + 60.77343007198517 + ], + [ + -12.091764695755302, + 59.86649718980701 + ], + [ + -13.251623400530494, + 58.91522972808762 + ], + [ + -14.370879910530354, + 57.92108744228152 + ], + [ + -15.448477245634779, + 56.88551055934222 + ], + [ + -16.48335598466102, + 55.809934424097946 + ], + [ + -17.47446647067688, + 54.695823674128555 + ], + [ + -18.420749282499617, + 53.54461853638747 + ], + [ + -19.32114988107176, + 52.35776411995351 + ], + [ + -20.17461372733583, + 51.13671529815592 + ], + [ + -20.980083841171723, + 49.88290741582297 + ], + [ + -21.736505683521976, + 48.59779534628392 + ], + [ + -22.442822274266465, + 47.28281931649227 + ], + [ + -23.097979074347734, + 45.93943419977733 + ], + [ + -23.700921544708308, + 44.56908510521784 + ], + [ + -24.250591484696756, + 43.17322202401783 + ], + [ + -24.74593557578693, + 41.75327541888024 + ], + [ + -25.185895617327397, + 40.310709927384934 + ], + [ + -25.569419511323336, + 38.84697065861065 + ], + [ + -25.89545149818594, + 37.36349783951088 + ], + [ + -26.162932156732477, + 35.8617463434149 + ], + [ + -26.3708093889681, + 34.34315151515097 + ], + [ + -26.51802621477272, + 32.80916822804836 + ], + [ + -26.603526874557538, + 31.261246473311093 + ], + [ + -26.626256829265078, + 29.70082647789267 + ], + [ + -26.626256829265078, + 11.865918801477871 + ], + [ + 0, + 0.08407080734751268 + ] + ] + }, + { + "type": "line", + "version": 337, + "versionNonce": 1619218060, + "isDeleted": false, + "id": "n0rqf1bsn46wLsUI73IjO", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2194.3454487369822, + "y": -2014.5074880864959, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 64.79416597169477, + "height": 78.53365069094667, + "seed": 1145565324, + "groupIds": [ + "ng1U6l9sAI234KMxvTZ2o", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -32.3910758358403, + 14.448078211167381 + ], + [ + -32.3910758358403, + 36.00609705655551 + ], + [ + -32.3910758358403, + 36.00609705655551 + ], + [ + -32.367369456079544, + 37.84777384749002 + ], + [ + -32.2678552659841, + 39.674028004708994 + ], + [ + -32.093853880438466, + 41.48310440417927 + ], + [ + -31.84668286299882, + 43.27322839336664 + ], + [ + -31.527663438815274, + 45.04263752505005 + ], + [ + -31.138114391975353, + 46.78956446988316 + ], + [ + -30.67935450656653, + 48.51224189851966 + ], + [ + -30.15270500773895, + 50.20890736373851 + ], + [ + -29.559482848783137, + 51.87779841831865 + ], + [ + -28.90100986511489, + 53.517137968663214 + ], + [ + -28.17860423055603, + 55.12516844967642 + ], + [ + -27.393585339459733, + 56.70012253201196 + ], + [ + -26.54727380671047, + 58.24023776844875 + ], + [ + -25.64098780613007, + 59.74373706538998 + ], + [ + -24.676047952603025, + 61.208867739865106 + ], + [ + -23.65377241995116, + 62.63385758040253 + ], + [ + -22.575480602527637, + 64.01693925765596 + ], + [ + -21.44249311521693, + 65.35635032440432 + ], + [ + -20.25613057290352, + 66.65032345130129 + ], + [ + -19.017711149409237, + 67.89709130900057 + ], + [ + -17.728551798024615, + 69.0948914502811 + ], + [ + -16.389973133634122, + 70.2419614279218 + ], + [ + -15.003298212184866, + 71.33651926620058 + ], + [ + -13.569842766436057, + 72.37681228214687 + ], + [ + -12.090927411272164, + 73.36107314641444 + ], + [ + -10.567872761577664, + 74.28753941178218 + ], + [ + -9.001996991174408, + 75.15443398465321 + ], + [ + -7.394618273884215, + 75.96000418205706 + ], + [ + -5.747059665654212, + 76.70247291039686 + ], + [ + -4.060636899243595, + 77.38008260457683 + ], + [ + -2.336670589536845, + 77.99106593525062 + ], + [ + -0.5764813514184316, + 78.53365069094667 + ], + [ + 0.6005014077275319, + 78.53365069094667 + ], + [ + 0.6005014077275319, + 78.53365069094667 + ], + [ + 2.353018385990306, + 77.99457618331694 + ], + [ + 4.06974450392746, + 77.38739114610014 + ], + [ + 5.749351823466625, + 76.71383361589105 + ], + [ + 7.3905368171617685, + 75.97567580416117 + ], + [ + 8.991981311191044, + 75.17466062963058 + ], + [ + 10.552362249607338, + 74.31256030377082 + ], + [ + 12.070371222839338, + 73.39111286317662 + ], + [ + 13.544694939190483, + 72.41209540144482 + ], + [ + 14.974020106964192, + 71.37725571942072 + ], + [ + 16.357023670213366, + 70.28834650007481 + ], + [ + 17.692392337241404, + 69.14713019062812 + ], + [ + 18.978817698477027, + 67.9553594740512 + ], + [ + 20.21498158009839, + 66.7148016796904 + ], + [ + 21.399565808283615, + 65.42720460839091 + ], + [ + 22.531252209210923, + 64.09432582524855 + ], + [ + 23.608732373308964, + 62.71793265960966 + ], + [ + 24.630693008881156, + 61.29976314806893 + ], + [ + 25.595815942105688, + 59.841589502097975 + ], + [ + 26.502782999160708, + 58.3451644046673 + ], + [ + 27.350280888349634, + 56.81225030299795 + ], + [ + 28.137006082226424, + 55.244594997935216 + ], + [ + 28.861625760593434, + 53.643960936700104 + ], + [ + 29.522826631754107, + 52.01210080226318 + ], + [ + 30.11930516826239, + 50.35077704184546 + ], + [ + 30.649738314171188, + 48.661747220542765 + ], + [ + 31.112807895658626, + 46.946759139200324 + ], + [ + 31.507210385278697, + 45.20758012716447 + ], + [ + 31.831622727084287, + 43.44596774953099 + ], + [ + 32.08473162937882, + 41.663669807145155 + ], + [ + 32.26521891834042, + 39.86244874722799 + ], + [ + 32.371781066523106, + 38.04406457593793 + ], + [ + 32.40309013585447, + 36.210269976245506 + ], + [ + 32.40309013585447, + 14.448078211167381 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 337, + "versionNonce": 1460121652, + "isDeleted": false, + "id": "peJDVA9mTcOvWBbtedtk9", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2194.3454487369822, + "y": -2008.1902074796785, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "width": 53.252513658530155, + "height": 66.25938635522165, + "seed": 1234304780, + "groupIds": [ + "JHrgg4sS7bNU14okiLtbP", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 26.626256829265078, + 11.78184860439602 + ], + [ + 26.626256829265078, + 29.652788806337103 + ], + [ + 26.626256829265078, + 29.652788806337103 + ], + [ + 26.604619250085825, + 31.214368306506216 + ], + [ + 26.52013407235635, + 32.76351547468527 + ], + [ + 26.373875363635207, + 34.29878764285813 + ], + [ + 26.166873252353522, + 35.818725055570226 + ], + [ + 25.900201806069838, + 37.32187772161756 + ], + [ + 25.574910681716354, + 38.806795649796086 + ], + [ + 25.192054418350562, + 40.272023966776544 + ], + [ + 24.752692437155186, + 41.71612244560541 + ], + [ + 24.257874395062434, + 43.13762644870285 + ], + [ + 23.708659713255045, + 44.535090866990146 + ], + [ + 23.10610781291576, + 45.90706570926324 + ], + [ + 22.451263468851515, + 47.252091220067584 + ], + [ + 21.745200748620857, + 48.56872717244969 + ], + [ + 20.98894978065492, + 49.855513810955 + ], + [ + 20.183584632512247, + 51.11100602650476 + ], + [ + 19.330159843250307, + 52.33374894576966 + ], + [ + 18.4297250698013, + 53.52228769542044 + ], + [ + 17.48333485122271, + 54.675177166378305 + ], + [ + 16.492048608697264, + 55.790962485313976 + ], + [ + 15.456920881282457, + 56.868193661023454 + ], + [ + 14.379006208035735, + 57.9054158201774 + ], + [ + 13.259368892265126, + 58.90118385369711 + ], + [ + 12.09904882665229, + 59.85404288825326 + ], + [ + 10.89911031450523, + 60.76254293264181 + ], + [ + 9.66060789488143, + 61.6252291135335 + ], + [ + 8.384600988963609, + 62.44065143972429 + ], + [ + 7.072144135809265, + 63.20735992001017 + ], + [ + 5.724286992350592, + 63.9239045631871 + ], + [ + 4.342088979770334, + 64.58883049592579 + ], + [ + 2.926599755000689, + 65.20068772702223 + ], + [ + 1.4788885034749268, + 65.7580213831471 + ], + [ + 0, + 66.25938635522165 + ], + [ + -1.477797348477959, + 65.7591882110857 + ], + [ + -2.9244906768857386, + 65.20307508627732 + ], + [ + -4.339025446165864, + 64.59250185412587 + ], + [ + -5.720344676198234, + 63.92892338796062 + ], + [ + -7.067391386862741, + 63.21377991473506 + ], + [ + -8.379113480164545, + 62.44851654352796 + ], + [ + -9.654453975983534, + 61.63458326554331 + ], + [ + -10.892353453136977, + 60.77343007198517 + ], + [ + -12.091764695755302, + 59.86649718980701 + ], + [ + -13.251623400530494, + 58.91522972808762 + ], + [ + -14.370879910530354, + 57.92108744228152 + ], + [ + -15.448477245634779, + 56.88551055934222 + ], + [ + -16.48335598466102, + 55.809934424097946 + ], + [ + -17.47446647067688, + 54.695823674128555 + ], + [ + -18.420749282499617, + 53.54461853638747 + ], + [ + -19.32114988107176, + 52.35776411995351 + ], + [ + -20.17461372733583, + 51.13671529815592 + ], + [ + -20.980083841171723, + 49.88290741582297 + ], + [ + -21.736505683521976, + 48.59779534628392 + ], + [ + -22.442822274266465, + 47.28281931649227 + ], + [ + -23.097979074347734, + 45.93943419977733 + ], + [ + -23.700921544708308, + 44.56908510521784 + ], + [ + -24.250591484696756, + 43.17322202401783 + ], + [ + -24.74593557578693, + 41.75327541888024 + ], + [ + -25.185895617327397, + 40.310709927384934 + ], + [ + -25.569419511323336, + 38.84697065861065 + ], + [ + -25.89545149818594, + 37.36349783951088 + ], + [ + -26.162932156732477, + 35.8617463434149 + ], + [ + -26.3708093889681, + 34.34315151515097 + ], + [ + -26.51802621477272, + 32.80916822804836 + ], + [ + -26.603526874557538, + 31.261246473311093 + ], + [ + -26.626256829265078, + 29.70082647789267 + ], + [ + -26.626256829265078, + 11.865918801477871 + ], + [ + 0, + 0.08407080734751268 + ] + ] + }, + { + "type": "line", + "version": 337, + "versionNonce": 1118936332, + "isDeleted": false, + "id": "OBVgG77jqBVttubO-8vdq", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2202.872578490964, + "y": -1967.1038596648962, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 23.59972973431832, + "height": 22.73500282506541, + "seed": 1737899404, + "groupIds": [ + "flen9nINK9vnXf-Fs5Ue0", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -19.51631527964584, + 19.61239062275698 + ], + [ + -19.51631527964584, + 19.61239062275698 + ], + [ + -18.30029748793496, + 20.463418288513278 + ], + [ + -17.048252052823056, + 21.26827569562828 + ], + [ + -15.760176533247499, + 22.02584971954132 + ], + [ + -14.436068488145656, + 22.73500282506541 + ], + [ + 4.083414454672477, + 4.119439657010865 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 337, + "versionNonce": 814451124, + "isDeleted": false, + "id": "KWoQ8q1bgV6O9ngOVNten", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2199.605845950801, + "y": -1983.5576153240688, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 29.892986928365477, + "height": 30.613588617638523, + "seed": 1737403404, + "groupIds": [ + "flen9nINK9vnXf-Fs5Ue0", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -25.797562445538446, + 25.941687665518323 + ], + [ + -25.797562445538446, + 25.941687665518323 + ], + [ + -24.980507048446146, + 27.159768155154122 + ], + [ + -24.116158503901357, + 28.345192108885136 + ], + [ + -23.206772353777005, + 29.49684640215069 + ], + [ + -22.254601698883377, + 30.613588617638523 + ], + [ + 4.095424482827033, + 4.1194420980735 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 337, + "versionNonce": 387062668, + "isDeleted": false, + "id": "REGazF5VKFCTQzMNRFKbP", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2183.2721930142357, + "y": -1986.896408033159, + "strokeColor": "#000000", + "backgroundColor": "#aecbfa", + "width": 19.095961853173932, + "height": 21.570032535136626, + "seed": 1534413452, + "groupIds": [ + "flen9nINK9vnXf-Fs5Ue0", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -15.012549839564088, + 15.07259753927421 + ], + [ + -15.012549839564088, + 15.07259753927421 + ], + [ + -14.688467041213968, + 16.736367244450225 + ], + [ + -14.293449403810369, + 18.375360163900897 + ], + [ + -13.828622257227131, + 19.987335402129094 + ], + [ + -13.295114592932032, + 21.570032535136626 + ], + [ + 4.083412013609845, + 4.119446980198766 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "ellipse", + "version": 337, + "versionNonce": 794309428, + "isDeleted": false, + "id": "2RGRHTUVawHblLxdmnPhC", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2199.3416253314003, + "y": -1970.5147125429141, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 11.145317112204834, + "height": 11.145317112204834, + "seed": 100931852, + "groupIds": [ + "e235bEG92vASATYdRDkSW", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 337, + "versionNonce": 937347596, + "isDeleted": false, + "id": "r41NRvAWP0G6p9PkQtixW", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2196.038867588899, + "y": -1986.896408033159, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 11.145317112204834, + "height": 11.145317112204834, + "seed": 1119164300, + "groupIds": [ + "e235bEG92vASATYdRDkSW", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 337, + "versionNonce": 1372826804, + "isDeleted": false, + "id": "4iFM40fUx6bWn2AckREZ7", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2179.7052097702085, + "y": -1990.2351983011868, + "strokeColor": "#000000", + "backgroundColor": "#669df6", + "width": 11.145317112204834, + "height": 11.145317112204834, + "seed": 1841448460, + "groupIds": [ + "e235bEG92vASATYdRDkSW", + "LA_S6hEfLFuoibT7K3arg", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693311607367, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1046, + "versionNonce": 945544060, + "isDeleted": false, + "id": "zokrrtrqCTA4sdSchyeKi", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2154.586397934257, + "y": -1931.2655151672457, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 79.36479187011719, + "height": 16.251850252262567, + "seed": 1277535372, + "groupIds": [ + "il6ujkh8lWpwImIyEFFwS", + "eL6pb70rQtosyYRIkkTz6", + "sUq8jzZlPCueNX8Vkw8R_", + "2oxJyo2US2MWfhDq8hBIX", + "YAXC5Z8rCERiRzJ07BtDR", + "BB12claUSzMP_AqjEGS7C", + "omvCuEw-yZaaMA9sUJfIC" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1693399360386, + "link": null, + "locked": false, + "fontSize": 14.144467472298855, + "fontFamily": 2, + "text": "Cloud Armor", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Cloud Armor", + "lineHeight": 1.1489898989898986, + "baseline": 13 + }, + { + "type": "rectangle", + "version": 158, + "versionNonce": 1795918644, + "isDeleted": false, + "id": "43t08qUBgpYV35dPr4FqE", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1490.9084466447387, + "y": -2228.622425041188, + "strokeColor": "#1864ab", + "backgroundColor": "#4285f4", + "width": 1157, + "height": 66, + "seed": 1070535860, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "aR0cFrtx68oO4Hn-Pp404" + } + ], + "updated": 1693319736943, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 143, + "versionNonce": 1168962060, + "isDeleted": false, + "id": "aR0cFrtx68oO4Hn-Pp404", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1742.4553216447387, + "y": -2217.2224250411878, + "strokeColor": "#ffffff", + "backgroundColor": "#4285f4", + "width": 653.90625, + "height": 43.199999999999996, + "seed": 276268940, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1693319736943, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 3, + "text": "Serverless PHPIPAM on Cloud Run", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "43t08qUBgpYV35dPr4FqE", + "originalText": "Serverless PHPIPAM on Cloud Run", + "lineHeight": 1.2, + "baseline": 35 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "af2541e7127d4fdc679914759de23d8bd87e9264": { + "mimeType": "image/png", + "id": "af2541e7127d4fdc679914759de23d8bd87e9264", + "dataURL": "data:image/png;base64,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", + "created": 1693297560288, + "lastRetrieved": 1693819633924 + }, + "7f10a90d0c745f95f1922694e27ad51a6bf7d09e": { + "mimeType": "image/png", + "id": "7f10a90d0c745f95f1922694e27ad51a6bf7d09e", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaAAAAPACAYAAADDhN6oAAAAAXNSR0IArs4c6QAAIABJREFUeF7s3QucXWV9N/pnrb33zOSeTAIk3AwyEEgQRJBwk9urtrbeqh5fq309Kt7xVttq37aeUytg/bRapWptXz1+qlW8oShFvAJSRNF6DdeAIUBICAm5Z257r7XOZ+2ZPUwimEkyz2TP5Ls/n5CQ7P1fz/qu/96z928/61lJcCNAgAABAgQIECBAgAABAgQIECBAgAABAhEEkgg1lSRAgAABAgQIECBAgAABAgQIECBAgAABAkEArQkIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECDWQuP3AAAgAElEQVRAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAgQIAAAQICaD1AgAABAgQIECBAgAABAgQIECBAgAABAlEEBNBRWBUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQG0HiBAgAABAgQIECBAgAABAgQIECBAgACBKAIC6CisihIgQIAAAQIECBAgQIAAAQIECBAgQICAAFoPECBAgAABAgQIECBAgAABAgQIECBAgEAUAQF0FFZFCRAgQIAAAQIECBAgQIAAAQIECBAgQEAArQcIECBAgAABAgQIECBAgAABAgQIECBAIIqAADoKq6IECBAgQIAAAQIECBAgQIAAAQIECBAgIIDWAwQIECBAgAABAgQIECBAgAABAgQIECAQRUAAHYVVUQIECBAgQIAAAQIECBAgQIAAAQIECBAQQOsBAgQIECBAgAABAgQIECBAgAABAgQIEIgiIICOwqooAQIECBAgQIAAAQIECBAgQIAAAQIECAig9QABAgQIECBAgAABAgQIECBAgAABAgQIRBEQQEdhVZQAAQIECBAgQIAAAQIECBAgQIAAAQIEBNB6gAABAgQIECBAgAABAgQIECBAgAABAgSiCAigo7AqSoAAAQIECBAgQIAAAQIECBAg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AAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCCAAAIIIIAAAggg4FoBAmjXloaGIYAAAggggAACCCCAAAIIIIAAAggggAAC3hYggPZ2/Wg9AggggAACCCCAAAIIIIAAAggggAACCCDgWgECaNeWhoYhgAACCCCAAAIIIIAAAggggAACCCCAAALeFiCA9nb9aD0CCCCAAAIIIIAAAggggAACCCCAAAIIIOBaAQJo15aGhiGAAAIIIIAAAggggAACCCCAAAIIIIAAAt4WIID2dv1oPQIIIIAAAggggAACCCDw/9uxYxoAAACEYf5dY2MkdUDKNwIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgQ4e5p8AABSfSURBVKyAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwjQ3/9ZT4AAAQIECBAgQIAAAQIECBAgQIAAgayAAJ29xjACBAgQIECAAAECBAgQIECAAAECBAh8CwwlSpAiDnlErAAAAABJRU5ErkJggg==", + "created": 1693297606675, + "lastRetrieved": 1693819633924 + } + } +} \ No newline at end of file diff --git a/blueprints/third-party-solutions/phpipam/glb.tf b/blueprints/third-party-solutions/phpipam/glb.tf new file mode 100644 index 00000000..9016330e --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/glb.tf @@ -0,0 +1,153 @@ +/** + * 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. + */ + +locals { + glb_create = var.phpipam_exposure == "EXTERNAL" + iap_sa_email = try(module.project.service_accounts.robots["iap"].email, "") +} + +# Reserved static IP for the Load Balancer +module "addresses" { + source = "../../../modules/net-address" + count = local.glb_create ? 1 : 0 + project_id = var.project_id + global_addresses = ["phpipam"] +} + +# Global L7 HTTPS Load Balancer in front of Cloud Run +module "glb" { + source = "../../../modules/net-lb-app-ext" + count = local.glb_create ? 1 : 0 + project_id = module.project.project_id + name = "phpipam-glb" + address = module.addresses.0.global_addresses["phpipam"].address + protocol = "HTTPS" + + backend_service_configs = { + default = { + backends = [ + { backend = "phpipam" } + ] + health_checks = [] + port_name = "http" + security_policy = try(google_compute_security_policy.policy[0].name, + null) + iap_config = try({ + oauth2_client_id = google_iap_client.iap_client[0].client_id, + oauth2_client_secret = google_iap_client.iap_client[0].secret + }, null) + } + } + health_check_configs = {} + neg_configs = { + phpipam = { + cloudrun = { + region = var.region + target_service = { + name = module.cloud_run.service_name + } + } + } + } + ssl_certificates = { + managed_configs = { + default = { + domains = [local.domain] + } + } + } +} + +# Cloud Armor configuration +resource "google_compute_security_policy" "policy" { + count = local.glb_create && var.security_policy.enabled ? 1 : 0 + project = module.project.project_id + name = "cloud-run-policy" + + rule { + action = "deny(403)" + priority = 1000 + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = var.security_policy.ip_blacklist + } + } + description = "Deny access to list of IPs" + } + rule { + action = "deny(403)" + priority = 900 + match { + expr { + expression = "request.path.matches(\"${var.security_policy.path_blocked}\")" + } + } + description = "Deny access to specific URL paths" + } + rule { + action = "allow" + priority = "2147483647" + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = ["*"] + } + } + description = "Default rule" + } +} + +# Identity-Aware Proxy (IAP) or OAuth brand (see OAuth consent screen) +# Note: +# Only "Organization Internal" brands can be created programmatically +# via API. To convert it into an external brand please use the GCP +# Console. +# Brands can only be created once for a Google Cloud project and the +# underlying Google API doesn't support DELETE or PATCH methods. +# Destroying a Terraform-managed Brand will remove it from state but +# will not delete it from Google Cloud. +resource "google_iap_brand" "iap_brand" { + count = local.glb_create && var.iap.enabled ? 1 : 0 + project = module.project.project_id + # Support email displayed on the OAuth consent screen. The caller must be + # the user with the associated email address, or if a group email is + # specified, the caller can be either a user or a service account which + # is an owner of the specified group in Cloud Identity. + support_email = var.iap.email + application_title = var.iap.app_title +} + +# IAP owned OAuth2 client +# Note: +# Only internal org clients can be created via declarative tools. +# External clients must be manually created via the GCP console. +# Warning: +# All arguments including secret will be stored in the raw state as plain-text. +resource "google_iap_client" "iap_client" { + count = local.glb_create && var.iap.enabled ? 1 : 0 + display_name = var.iap.oauth2_client_name + brand = google_iap_brand.iap_brand[0].name +} + +# IAM policy for IAP +# For simplicity we use the same email as support_email and authorized member +resource "google_iap_web_iam_member" "iap_iam" { + count = local.glb_create && var.iap.enabled ? 1 : 0 + project = module.project.project_id + role = "roles/iap.httpsResourceAccessor" + member = "user:${var.iap.email}" +} diff --git a/blueprints/third-party-solutions/phpipam/ilb.tf b/blueprints/third-party-solutions/phpipam/ilb.tf new file mode 100644 index 00000000..814f937f --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/ilb.tf @@ -0,0 +1,89 @@ +/** + * 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. + */ + +locals { + ilb_create = var.phpipam_exposure == "INTERNAL" +} + +# default ssl certificate +resource "tls_private_key" "default" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "default" { + private_key_pem = tls_private_key.default.private_key_pem + validity_period_hours = 720 + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth", + ] + subject { + common_name = local.domain + organization = "ACME Examples, Inc" + } +} + +module "ilb-l7" { + source = "../../../modules/net-lb-app-int" + count = local.ilb_create ? 1 : 0 + project_id = var.project_id + name = "ilb-l7-cr" + protocol = "HTTPS" + region = var.region + + backend_service_configs = { + default = { + project_id = var.project_id + backends = [ + { + group = "phpipam" + } + ] + health_checks = [] + } + } + health_check_configs = { + default = { + https = { port = 443 } + } + } + neg_configs = { + phpipam = { + project_id = var.project_id + cloudrun = { + region = var.region + target_service = { + name = module.cloud_run.service_name + } + } + } + } + ssl_certificates = { + create_configs = { + default = { + # certificate and key could also be read via file() from external files + certificate = tls_self_signed_cert.default.cert_pem + private_key = tls_private_key.default.private_key_pem + } + } + } + vpc_config = { + network = local.network + subnetwork = local.subnetwork + } +} diff --git a/blueprints/third-party-solutions/phpipam/images/phpipam.png b/blueprints/third-party-solutions/phpipam/images/phpipam.png new file mode 100644 index 00000000..6d032778 Binary files /dev/null and b/blueprints/third-party-solutions/phpipam/images/phpipam.png differ diff --git a/blueprints/third-party-solutions/phpipam/images/phpipam_admin.png b/blueprints/third-party-solutions/phpipam/images/phpipam_admin.png new file mode 100644 index 00000000..aea68b03 Binary files /dev/null and b/blueprints/third-party-solutions/phpipam/images/phpipam_admin.png differ diff --git a/blueprints/third-party-solutions/phpipam/images/phpipam_db.png b/blueprints/third-party-solutions/phpipam/images/phpipam_db.png new file mode 100644 index 00000000..9d218a42 Binary files /dev/null and b/blueprints/third-party-solutions/phpipam/images/phpipam_db.png differ diff --git a/blueprints/third-party-solutions/phpipam/images/phpipam_home.png b/blueprints/third-party-solutions/phpipam/images/phpipam_home.png new file mode 100644 index 00000000..49168616 Binary files /dev/null and b/blueprints/third-party-solutions/phpipam/images/phpipam_home.png differ diff --git a/blueprints/third-party-solutions/phpipam/images/phpipam_install.png b/blueprints/third-party-solutions/phpipam/images/phpipam_install.png new file mode 100644 index 00000000..35835cc9 Binary files /dev/null and b/blueprints/third-party-solutions/phpipam/images/phpipam_install.png differ diff --git a/blueprints/third-party-solutions/phpipam/main.tf b/blueprints/third-party-solutions/phpipam/main.tf new file mode 100644 index 00000000..7998dfa2 --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/main.tf @@ -0,0 +1,144 @@ +/** + * 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. + */ + +locals { + cloudsql_conf = { + database_version = "MYSQL_8_0" + tier = "db-g1-small" + db = "phpipam" + user = "admin" + } + connector = var.connector == null ? module.cloud_run.vpc_connector : var.connector + domain = ( + var.custom_domain != null ? var.custom_domain : ( + var.phpipam_exposure == "EXTERNAL" ? + "${module.addresses.0.global_addresses["phpipam"].address}.nip.io" : "phpipam.internal") + ) + iam = { + # CloudSQL + "roles/cloudsql.admin" = var.admin_principals + "roles/cloudsql.client" = var.admin_principals + "roles/cloudsql.instanceUser" = var.admin_principals + # common roles + "roles/logging.admin" = var.admin_principals + "roles/iam.serviceAccountUser" = var.admin_principals + "roles/iam.serviceAccountTokenCreator" = var.admin_principals + } + network = var.vpc_config == null ? module.vpc.0.self_link : var.vpc_config.network + phpipam_password = var.phpipam_password == null ? random_password.phpipam_password.result : var.phpipam_password + subnetwork = var.vpc_config == null ? module.vpc.0.subnet_self_links["${var.region}/ilb"] : var.vpc_config.subnetwork +} + + +# either create a project or set up the given one +module "project" { + source = "../../../modules/project" + billing_account = try(var.project_create.billing_account_id, null) + iam = var.project_create != null ? local.iam : {} + name = var.project_id + parent = try(var.project_create.parent, null) + prefix = var.project_create == null ? null : var.prefix + project_create = var.project_create != null + services = [ + "iap.googleapis.com", + "logging.googleapis.com", + "monitoring.googleapis.com", + "run.googleapis.com", + "servicenetworking.googleapis.com", + "sqladmin.googleapis.com", + "sql-component.googleapis.com", + "vpcaccess.googleapis.com" + ] +} + + +# create a VPC for CloudSQL and ILB +module "vpc" { + source = "../../../modules/net-vpc" + count = var.vpc_config == null ? 1 : 0 + project_id = module.project.project_id + name = "${var.prefix}-sql-vpc" + + psa_config = { + ranges = { + cloud-sql = var.ip_ranges.psa + } + } + subnets = [ + { + ip_cidr_range = var.ip_ranges.ilb + name = "ilb" + region = var.region + } + ] +} + +resource "random_password" "phpipam_password" { + length = 8 +} + +# create the Cloud Run service +module "cloud_run" { + source = "../../../modules/cloud-run" + project_id = module.project.project_id + name = "${var.prefix}-cr-phpipam" + prefix = var.prefix + ingress_settings = "all" + region = var.region + + containers = { + phpipam = { + image = var.phpipam_config.image + ports = { + http = { + name = "http1" + protocol = null + container_port = var.phpipam_config.port + } + } + env_from = null + # set up the database connection + env = { + "TZ" = "Europe/Rome" + "IPAM_DATABASE_HOST" = module.cloudsql.ip + "IPAM_DATABASE_USER" = local.cloudsql_conf.user + "IPAM_DATABASE_PASS" = var.cloudsql_password == null ? module.cloudsql.user_passwords[local.cloudsql_conf.user] : var.cloudsql_password + "IPAM_DATABASE_NAME" = local.cloudsql_conf.db + "IPAM_DATABASE_PORT" = "3306" + } + } + } + iam = local.glb_create && var.iap.enabled ? { + "roles/run.invoker" : ["serviceAccount:${local.iap_sa_email}"] + } : { + "roles/run.invoker" : [var.cloud_run_invoker] + } + revision_annotations = { + autoscaling = { + min_scale = 1 + max_scale = 2 + } + # connect to CloudSQL + cloudsql_instances = [module.cloudsql.connection_name] + # allow all traffic + vpcaccess_egress = "private-ranges-only" + vpcaccess_connector = local.connector + } + vpc_connector_create = var.create_connector ? { + ip_cidr_range = var.ip_ranges.connector + vpc_self_link = local.network + } : null +} diff --git a/blueprints/third-party-solutions/phpipam/outputs.tf b/blueprints/third-party-solutions/phpipam/outputs.tf new file mode 100644 index 00000000..0795c0f2 --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/outputs.tf @@ -0,0 +1,48 @@ +/** + * 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. + */ + +output "cloud_run_service" { + description = "CloudRun service URL." + value = module.cloud_run.service.status[0].url + sensitive = true +} + +output "cloudsql_password" { + description = "CloudSQL password." + value = var.cloudsql_password == null ? module.cloudsql.user_passwords[local.cloudsql_conf.user] : var.cloudsql_password + sensitive = true +} + +output "phpipam_ip_address" { + description = "PHPIPAM IP Address either external or internal according to app exposure." + value = local.glb_create ? module.addresses.0.global_addresses["phpipam"].address : module.ilb-l7.0.address +} + +output "phpipam_password" { + description = "PHPIPAM user password." + value = local.phpipam_password + sensitive = true +} + +output "phpipam_url" { + description = "PHPIPAM website url." + value = local.domain +} + +output "phpipam_user" { + description = "PHPIPAM username." + value = "admin" +} diff --git a/blueprints/third-party-solutions/phpipam/terraform.tfvars.sample b/blueprints/third-party-solutions/phpipam/terraform.tfvars.sample new file mode 100644 index 00000000..776bedf9 --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/terraform.tfvars.sample @@ -0,0 +1,2 @@ +prefix = "phpipam" +project_id = "my-phpipam-project" diff --git a/blueprints/third-party-solutions/phpipam/variables.tf b/blueprints/third-party-solutions/phpipam/variables.tf new file mode 100644 index 00000000..75d3d2c6 --- /dev/null +++ b/blueprints/third-party-solutions/phpipam/variables.tf @@ -0,0 +1,156 @@ +/** + * 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. + */ + +# Documentation: https://cloud.google.com/run/docs/securing/managing-access#making_a_service_public + +variable "admin_principals" { + description = "Users, groups and/or service accounts that are assigned roles, in IAM format (`group:foo@example.com`)." + type = list(string) + default = [] +} + +variable "cloud_run_invoker" { + description = "IAM member authorized to access the end-point (for example, 'user:YOUR_IAM_USER' for only you or 'allUsers' for everyone)." + type = string + default = "allUsers" +} + +variable "cloudsql_password" { + description = "CloudSQL password (will be randomly generated by default)." + type = string + default = null +} + +variable "connector" { + description = "Existing VPC serverless connector to use if not creating a new one." + type = string + default = null +} + +variable "create_connector" { + description = "Should a VPC serverless connector be created or not." + type = bool + default = true +} + +variable "custom_domain" { + description = "Cloud Run service custom domain for GLB." + type = string + default = null +} + +variable "iap" { + description = "Identity-Aware Proxy for Cloud Run in the LB." + type = object({ + enabled = optional(bool, false) + app_title = optional(string, "Cloud Run Explore Application") + oauth2_client_name = optional(string, "Test Client") + email = optional(string) + }) + default = {} +} + +# PSA: documentation: https://cloud.google.com/vpc/docs/configure-private-services-access#allocating-range +variable "ip_ranges" { + description = "CIDR blocks: VPC serverless connector, Private Service Access(PSA) for CloudSQL, CloudSQL VPC." + type = object({ + connector = string + psa = string + ilb = string + }) + default = { + connector = "10.8.0.0/28" + psa = "10.60.0.0/24" + ilb = "10.128.0.0/28" + } +} + +variable "phpipam_config" { + description = "PHPIpam configuration." + type = object({ + image = optional(string, "phpipam/phpipam-www:latest") + port = optional(number, 80) + }) + default = { + image = "phpipam/phpipam-www:latest" + port = 80 + } +} + +variable "phpipam_exposure" { + description = "Whether to expose the application publicly via GLB or internally via ILB, default GLB." + type = string + default = "EXTERNAL" + validation { + condition = var.phpipam_exposure == "INTERNAL" || var.phpipam_exposure == "EXTERNAL" + error_message = "phpipam_exposure supports only 'INTERNAL' or 'EXTERNAL'" + } +} + +variable "phpipam_password" { + description = "Password for the phpipam user (will be randomly generated by default)." + type = string + default = null +} + +variable "prefix" { + description = "Prefix used for resource names." + type = string + nullable = false + validation { + condition = var.prefix != "" + error_message = "Prefix cannot be empty." + } +} + +variable "project_create" { + description = "Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format." + type = object({ + billing_account_id = string + parent = string + }) + default = null +} + +variable "project_id" { + description = "Project id, references existing project if `project_create` is null." + type = string +} + +variable "region" { + description = "Region for the created resources." + type = string + default = "europe-west4" +} + +variable "security_policy" { + description = "Security policy (Cloud Armor) to enforce in the LB." + type = object({ + enabled = optional(bool, false) + ip_blacklist = optional(list(string), ["*"]) + path_blocked = optional(string, "/login.html") + }) + default = {} +} + +variable "vpc_config" { + description = "VPC Network and subnetwork self links for internal LB setup." + type = object({ + network = string + subnetwork = string + }) + default = null +} diff --git a/default-versions.tf b/default-versions.tf index f494b243..91a91a31 100644 --- a/default-versions.tf +++ b/default-versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/fast/stages/0-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf index 946e3d7b..d9f62221 100644 --- a/fast/stages/0-bootstrap/organization.tf +++ b/fast/stages/0-bootstrap/organization.tf @@ -88,8 +88,9 @@ module "organization" { ) # delegated role grant for resource manager service account iam_bindings = { - (module.organization.custom_role_id[var.custom_role_names.organization_iam_admin]) = { + organization_iam_admin_conditional = { members = [module.automation-tf-resman-sa.iam_email] + role = module.organization.custom_role_id[var.custom_role_names.organization_iam_admin] condition = { expression = format( "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", diff --git a/fast/stages/1-resman/outputs.tf b/fast/stages/1-resman/outputs.tf index 41994e98..fcfa4ff3 100644 --- a/fast/stages/1-resman/outputs.tf +++ b/fast/stages/1-resman/outputs.tf @@ -223,9 +223,9 @@ locals { tfvars = { folder_ids = local.folder_ids service_accounts = local.service_accounts - tag_keys = { for k, v in module.organization.tag_keys : k => v.id } + tag_keys = { for k, v in try(module.organization.tag_keys, {}) : k => v.id } tag_names = var.tag_names - tag_values = { for k, v in module.organization.tag_values : k => v.id } + tag_values = { for k, v in try(module.organization.tag_values, {}) : k => v.id } } } diff --git a/fast/stages/2-networking-a-peering/README.md b/fast/stages/2-networking-a-peering/README.md index 75c5fb66..f536b943 100644 --- a/fast/stages/2-networking-a-peering/README.md +++ b/fast/stages/2-networking-a-peering/README.md @@ -406,10 +406,10 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [factories_config](variables.tf#L80) | Configuration for network resource factories. | object({…}) | | {…} | | | [outputs_location](variables.tf#L121) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | | [peering_configs](variables-peerings.tf#L19) | Peering configurations. | object({…}) | | {} | | -| [psa_ranges](variables.tf#L138) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | | -| [regions](variables.tf#L159) | Region definitions. | object({…}) | | {…} | | -| [service_accounts](variables.tf#L171) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | -| [vpn_onprem_primary_config](variables.tf#L185) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | +| [psa_ranges](variables.tf#L138) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | | +| [regions](variables.tf#L155) | Region definitions. | object({…}) | | {…} | | +| [service_accounts](variables.tf#L167) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_primary_config](variables.tf#L181) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | ## Outputs diff --git a/fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml index ad5a06d5..444903eb 100644 --- a/fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml +++ b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml @@ -4,5 +4,5 @@ region: europe-west1 description: Default subnet for dev Data Platform ip_cidr_range: 10.127.48.0/24 secondary_ip_ranges: - pods: 100.64.0.0/24 + pods: 100.64.0.0/16 services: 100.64.1.0/24 diff --git a/fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml index 9844d0f0..74ca5f42 100644 --- a/fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml +++ b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml @@ -4,5 +4,5 @@ region: europe-west1 description: Default subnet for prod gke nodes ip_cidr_range: 10.127.49.0/24 secondary_ip_ranges: - pods: 100.65.0.0/24 + pods: 100.65.0.0/16 services: 100.65.1.0/24 diff --git a/fast/stages/2-networking-a-peering/landing.tf b/fast/stages/2-networking-a-peering/landing.tf index 013c6e86..e2309f1b 100644 --- a/fast/stages/2-networking-a-peering/landing.tf +++ b/fast/stages/2-networking-a-peering/landing.tf @@ -55,7 +55,9 @@ module "landing-vpc" { private = true restricted = true } - data_folder = "${var.factories_config.data_dir}/subnets/landing" + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/landing" + } } module "landing-firewall" { diff --git a/fast/stages/2-networking-a-peering/spoke-dev.tf b/fast/stages/2-networking-a-peering/spoke-dev.tf index 838ba6a4..bfff002b 100644 --- a/fast/stages/2-networking-a-peering/spoke-dev.tf +++ b/fast/stages/2-networking-a-peering/spoke-dev.tf @@ -46,12 +46,14 @@ module "dev-spoke-project" { } module "dev-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.dev-spoke-project.project_id - name = "dev-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/dev" - psa_config = try(var.psa_ranges.dev, null) + source = "../../../modules/net-vpc" + project_id = module.dev-spoke-project.project_id + name = "dev-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/dev" + } + psa_config = try(var.psa_ranges.dev, null) # set explicit routes for googleapis in case the default route is deleted create_googleapis_routes = { private = true diff --git a/fast/stages/2-networking-a-peering/spoke-prod.tf b/fast/stages/2-networking-a-peering/spoke-prod.tf index 7569647e..505005bd 100644 --- a/fast/stages/2-networking-a-peering/spoke-prod.tf +++ b/fast/stages/2-networking-a-peering/spoke-prod.tf @@ -45,12 +45,14 @@ module "prod-spoke-project" { } module "prod-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.prod-spoke-project.project_id - name = "prod-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/prod" - psa_config = try(var.psa_ranges.prod, null) + source = "../../../modules/net-vpc" + project_id = module.prod-spoke-project.project_id + name = "prod-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/prod" + } + psa_config = try(var.psa_ranges.prod, null) # set explicit routes for googleapis in case the default route is deleted create_googleapis_routes = { private = true diff --git a/fast/stages/2-networking-a-peering/variables.tf b/fast/stages/2-networking-a-peering/variables.tf index a0ff0a79..d0190dfa 100644 --- a/fast/stages/2-networking-a-peering/variables.tf +++ b/fast/stages/2-networking-a-peering/variables.tf @@ -139,18 +139,14 @@ variable "psa_ranges" { description = "IP ranges used for Private Service Access (CloudSQL, etc.)." type = object({ dev = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) prod = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) }) default = null diff --git a/fast/stages/2-networking-b-vpn/README.md b/fast/stages/2-networking-b-vpn/README.md index e87cee14..3cbf75f3 100644 --- a/fast/stages/2-networking-b-vpn/README.md +++ b/fast/stages/2-networking-b-vpn/README.md @@ -430,11 +430,11 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [dns](variables.tf#L72) | Onprem DNS resolvers. | map(list(string)) | | {…} | | | [factories_config](variables.tf#L80) | Configuration for network resource factories. | object({…}) | | {…} | | | [outputs_location](variables.tf#L121) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L138) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | | -| [regions](variables.tf#L159) | Region definitions. | object({…}) | | {…} | | -| [service_accounts](variables.tf#L171) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [psa_ranges](variables.tf#L138) | IP ranges used for Private Service Access (CloudSQL, etc.). | object({…}) | | null | | +| [regions](variables.tf#L155) | Region definitions. | object({…}) | | {…} | | +| [service_accounts](variables.tf#L167) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | | [vpn_configs](variables-vpn.tf#L17) | Hub to spokes VPN configurations. | object({…}) | | {…} | | -| [vpn_onprem_primary_config](variables.tf#L185) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | +| [vpn_onprem_primary_config](variables.tf#L181) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | ## Outputs diff --git a/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml index ad5a06d5..444903eb 100644 --- a/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml +++ b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml @@ -4,5 +4,5 @@ region: europe-west1 description: Default subnet for dev Data Platform ip_cidr_range: 10.127.48.0/24 secondary_ip_ranges: - pods: 100.64.0.0/24 + pods: 100.64.0.0/16 services: 100.64.1.0/24 diff --git a/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml index 9844d0f0..74ca5f42 100644 --- a/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml +++ b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml @@ -4,5 +4,5 @@ region: europe-west1 description: Default subnet for prod gke nodes ip_cidr_range: 10.127.49.0/24 secondary_ip_ranges: - pods: 100.65.0.0/24 + pods: 100.65.0.0/16 services: 100.65.1.0/24 diff --git a/fast/stages/2-networking-b-vpn/landing.tf b/fast/stages/2-networking-b-vpn/landing.tf index 013c6e86..e2309f1b 100644 --- a/fast/stages/2-networking-b-vpn/landing.tf +++ b/fast/stages/2-networking-b-vpn/landing.tf @@ -55,7 +55,9 @@ module "landing-vpc" { private = true restricted = true } - data_folder = "${var.factories_config.data_dir}/subnets/landing" + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/landing" + } } module "landing-firewall" { diff --git a/fast/stages/2-networking-b-vpn/spoke-dev.tf b/fast/stages/2-networking-b-vpn/spoke-dev.tf index 838ba6a4..bfff002b 100644 --- a/fast/stages/2-networking-b-vpn/spoke-dev.tf +++ b/fast/stages/2-networking-b-vpn/spoke-dev.tf @@ -46,12 +46,14 @@ module "dev-spoke-project" { } module "dev-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.dev-spoke-project.project_id - name = "dev-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/dev" - psa_config = try(var.psa_ranges.dev, null) + source = "../../../modules/net-vpc" + project_id = module.dev-spoke-project.project_id + name = "dev-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/dev" + } + psa_config = try(var.psa_ranges.dev, null) # set explicit routes for googleapis in case the default route is deleted create_googleapis_routes = { private = true diff --git a/fast/stages/2-networking-b-vpn/spoke-prod.tf b/fast/stages/2-networking-b-vpn/spoke-prod.tf index 7569647e..505005bd 100644 --- a/fast/stages/2-networking-b-vpn/spoke-prod.tf +++ b/fast/stages/2-networking-b-vpn/spoke-prod.tf @@ -45,12 +45,14 @@ module "prod-spoke-project" { } module "prod-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.prod-spoke-project.project_id - name = "prod-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/prod" - psa_config = try(var.psa_ranges.prod, null) + source = "../../../modules/net-vpc" + project_id = module.prod-spoke-project.project_id + name = "prod-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/prod" + } + psa_config = try(var.psa_ranges.prod, null) # set explicit routes for googleapis in case the default route is deleted create_googleapis_routes = { private = true diff --git a/fast/stages/2-networking-b-vpn/variables.tf b/fast/stages/2-networking-b-vpn/variables.tf index a0ff0a79..d0190dfa 100644 --- a/fast/stages/2-networking-b-vpn/variables.tf +++ b/fast/stages/2-networking-b-vpn/variables.tf @@ -139,18 +139,14 @@ variable "psa_ranges" { description = "IP ranges used for Private Service Access (CloudSQL, etc.)." type = object({ dev = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) prod = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) }) default = null diff --git a/fast/stages/2-networking-c-nva/README.md b/fast/stages/2-networking-c-nva/README.md index dfc41a0c..5d7cc9b4 100644 --- a/fast/stages/2-networking-c-nva/README.md +++ b/fast/stages/2-networking-c-nva/README.md @@ -488,11 +488,11 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [gcp_ranges](variables.tf#L111) | GCP address ranges in name => range format. | map(string) | | {…} | | | [onprem_cidr](variables.tf#L126) | Onprem addresses in name => range format. | map(string) | | {…} | | | [outputs_location](variables.tf#L144) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L161) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | | -| [regions](variables.tf#L182) | Region definitions. | object({…}) | | {…} | | -| [service_accounts](variables.tf#L194) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | -| [vpn_onprem_primary_config](variables.tf#L208) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | -| [vpn_onprem_secondary_config](variables.tf#L251) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | | +| [psa_ranges](variables.tf#L161) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | | +| [regions](variables.tf#L178) | Region definitions. | object({…}) | | {…} | | +| [service_accounts](variables.tf#L190) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_primary_config](variables.tf#L204) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | +| [vpn_onprem_secondary_config](variables.tf#L247) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | | ## Outputs diff --git a/fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml index ad5a06d5..444903eb 100644 --- a/fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml +++ b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml @@ -4,5 +4,5 @@ region: europe-west1 description: Default subnet for dev Data Platform ip_cidr_range: 10.127.48.0/24 secondary_ip_ranges: - pods: 100.64.0.0/24 + pods: 100.64.0.0/16 services: 100.64.1.0/24 diff --git a/fast/stages/2-networking-c-nva/landing.tf b/fast/stages/2-networking-c-nva/landing.tf index e7329a43..fb19c31b 100644 --- a/fast/stages/2-networking-c-nva/landing.tf +++ b/fast/stages/2-networking-c-nva/landing.tf @@ -54,7 +54,9 @@ module "landing-untrusted-vpc" { logging = false } create_googleapis_routes = null - data_folder = "${var.factories_config.data_dir}/subnets/landing-untrusted" + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/landing-untrusted" + } } module "landing-untrusted-firewall" { @@ -110,7 +112,9 @@ module "landing-trusted-vpc" { name = "prod-trusted-landing-0" delete_default_routes_on_create = true mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/landing-trusted" + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/landing-trusted" + } dns_policy = { inbound = true } diff --git a/fast/stages/2-networking-c-nva/spoke-dev.tf b/fast/stages/2-networking-c-nva/spoke-dev.tf index a90d25aa..0f6e8b8f 100644 --- a/fast/stages/2-networking-c-nva/spoke-dev.tf +++ b/fast/stages/2-networking-c-nva/spoke-dev.tf @@ -45,11 +45,13 @@ module "dev-spoke-project" { } module "dev-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.dev-spoke-project.project_id - name = "dev-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/dev" + source = "../../../modules/net-vpc" + project_id = module.dev-spoke-project.project_id + name = "dev-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/dev" + } delete_default_routes_on_create = true psa_config = try(var.psa_ranges.dev, null) # Set explicit routes for googleapis; send everything else to NVAs diff --git a/fast/stages/2-networking-c-nva/spoke-prod.tf b/fast/stages/2-networking-c-nva/spoke-prod.tf index 8dd5af44..98959509 100644 --- a/fast/stages/2-networking-c-nva/spoke-prod.tf +++ b/fast/stages/2-networking-c-nva/spoke-prod.tf @@ -44,11 +44,13 @@ module "prod-spoke-project" { } module "prod-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.prod-spoke-project.project_id - name = "prod-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/prod" + source = "../../../modules/net-vpc" + project_id = module.prod-spoke-project.project_id + name = "prod-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/prod" + } delete_default_routes_on_create = true psa_config = try(var.psa_ranges.prod, null) # Set explicit routes for googleapis; send everything else to NVAs diff --git a/fast/stages/2-networking-c-nva/variables.tf b/fast/stages/2-networking-c-nva/variables.tf index 1b4ad4ec..67697a22 100644 --- a/fast/stages/2-networking-c-nva/variables.tf +++ b/fast/stages/2-networking-c-nva/variables.tf @@ -162,18 +162,14 @@ variable "psa_ranges" { description = "IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format." type = object({ dev = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) prod = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) }) default = null diff --git a/fast/stages/2-networking-d-separate-envs/README.md b/fast/stages/2-networking-d-separate-envs/README.md index 7514454f..31a69ef6 100644 --- a/fast/stages/2-networking-d-separate-envs/README.md +++ b/fast/stages/2-networking-d-separate-envs/README.md @@ -348,11 +348,11 @@ Regions are defined via the `regions` variable which sets up a mapping between t | [dns](variables.tf#L72) | Onprem DNS resolvers. | map(list(string)) | | {…} | | | [factories_config](variables.tf#L81) | Configuration for network resource factories. | object({…}) | | {…} | | | [outputs_location](variables.tf#L122) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L139) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [regions](variables.tf#L160) | Region definitions. | object({…}) | | {…} | | -| [service_accounts](variables.tf#L170) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | -| [vpn_onprem_dev_primary_config](variables.tf#L184) | VPN gateway configuration for onprem interconnection from dev in the primary region. | object({…}) | | null | | -| [vpn_onprem_prod_primary_config](variables.tf#L227) | VPN gateway configuration for onprem interconnection from prod in the primary region. | object({…}) | | null | | +| [psa_ranges](variables.tf#L139) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [regions](variables.tf#L156) | Region definitions. | object({…}) | | {…} | | +| [service_accounts](variables.tf#L166) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_dev_primary_config](variables.tf#L180) | VPN gateway configuration for onprem interconnection from dev in the primary region. | object({…}) | | null | | +| [vpn_onprem_prod_primary_config](variables.tf#L223) | VPN gateway configuration for onprem interconnection from prod in the primary region. | object({…}) | | null | | ## Outputs diff --git a/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml index ad5a06d5..444903eb 100644 --- a/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml +++ b/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml @@ -4,5 +4,5 @@ region: europe-west1 description: Default subnet for dev Data Platform ip_cidr_range: 10.127.48.0/24 secondary_ip_ranges: - pods: 100.64.0.0/24 + pods: 100.64.0.0/16 services: 100.64.1.0/24 diff --git a/fast/stages/2-networking-d-separate-envs/spoke-dev.tf b/fast/stages/2-networking-d-separate-envs/spoke-dev.tf index b5b485be..61562f44 100644 --- a/fast/stages/2-networking-d-separate-envs/spoke-dev.tf +++ b/fast/stages/2-networking-d-separate-envs/spoke-dev.tf @@ -46,12 +46,14 @@ module "dev-spoke-project" { } module "dev-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.dev-spoke-project.project_id - name = "dev-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/dev" - psa_config = try(var.psa_ranges.dev, null) + source = "../../../modules/net-vpc" + project_id = module.dev-spoke-project.project_id + name = "dev-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/dev" + } + psa_config = try(var.psa_ranges.dev, null) # set explicit routes for googleapis in case the default route is deleted create_googleapis_routes = { private = true diff --git a/fast/stages/2-networking-d-separate-envs/spoke-prod.tf b/fast/stages/2-networking-d-separate-envs/spoke-prod.tf index bf43728d..7b42f546 100644 --- a/fast/stages/2-networking-d-separate-envs/spoke-prod.tf +++ b/fast/stages/2-networking-d-separate-envs/spoke-prod.tf @@ -45,12 +45,14 @@ module "prod-spoke-project" { } module "prod-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.prod-spoke-project.project_id - name = "prod-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/prod" - psa_config = try(var.psa_ranges.prod, null) + source = "../../../modules/net-vpc" + project_id = module.prod-spoke-project.project_id + name = "prod-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/prod" + } + psa_config = try(var.psa_ranges.prod, null) # set explicit routes for googleapis in case the default route is deleted create_googleapis_routes = { private = true diff --git a/fast/stages/2-networking-d-separate-envs/variables.tf b/fast/stages/2-networking-d-separate-envs/variables.tf index 29d4788a..8edcd72c 100644 --- a/fast/stages/2-networking-d-separate-envs/variables.tf +++ b/fast/stages/2-networking-d-separate-envs/variables.tf @@ -140,18 +140,14 @@ variable "psa_ranges" { description = "IP ranges used for Private Service Access (e.g. CloudSQL)." type = object({ dev = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) prod = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) }) default = null diff --git a/fast/stages/2-networking-e-nva-bgp/README.md b/fast/stages/2-networking-e-nva-bgp/README.md index eabd74db..be1526c8 100644 --- a/fast/stages/2-networking-e-nva-bgp/README.md +++ b/fast/stages/2-networking-e-nva-bgp/README.md @@ -515,12 +515,12 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [ncc_asn](variables.tf#L126) | The NCC Cloud Routers ASN configuration. | map(number) | | {…} | | | [onprem_cidr](variables.tf#L137) | Onprem addresses in name => range format. | map(string) | | {…} | | | [outputs_location](variables.tf#L155) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L172) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | | -| [regions](variables.tf#L193) | Region definitions. | object({…}) | | {…} | | -| [service_accounts](variables.tf#L205) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | -| [vpn_onprem_primary_config](variables.tf#L219) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | -| [vpn_onprem_secondary_config](variables.tf#L262) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | | -| [zones](variables.tf#L305) | Zones in which NVAs are deployed. | list(string) | | ["b", "c"] | | +| [psa_ranges](variables.tf#L172) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | | +| [regions](variables.tf#L189) | Region definitions. | object({…}) | | {…} | | +| [service_accounts](variables.tf#L201) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_primary_config](variables.tf#L215) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | +| [vpn_onprem_secondary_config](variables.tf#L258) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | | +| [zones](variables.tf#L301) | Zones in which NVAs are deployed. | list(string) | | ["b", "c"] | | ## Outputs diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml index cdb41e3f..1a8596b0 100644 --- a/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml @@ -4,5 +4,5 @@ region: europe-west1 description: Default subnet for dev Data Platform ip_cidr_range: 10.127.48.0/24 secondary_ip_ranges: - pods: 100.64.0.0/24 + pods: 100.64.0.0/16 services: 100.64.1.0/24 diff --git a/fast/stages/2-networking-e-nva-bgp/landing.tf b/fast/stages/2-networking-e-nva-bgp/landing.tf index ab6c94eb..07331717 100644 --- a/fast/stages/2-networking-e-nva-bgp/landing.tf +++ b/fast/stages/2-networking-e-nva-bgp/landing.tf @@ -55,7 +55,9 @@ module "landing-untrusted-vpc" { logging = false } create_googleapis_routes = null - data_folder = "${var.factories_config.data_dir}/subnets/landing-untrusted" + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/landing-untrusted" + } } module "landing-untrusted-firewall" { @@ -111,7 +113,9 @@ module "landing-trusted-vpc" { name = "prod-trusted-landing-0" delete_default_routes_on_create = true mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/landing-trusted" + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/landing-trusted" + } dns_policy = { inbound = true } diff --git a/fast/stages/2-networking-e-nva-bgp/spoke-dev.tf b/fast/stages/2-networking-e-nva-bgp/spoke-dev.tf index 0c70b550..56b65e39 100644 --- a/fast/stages/2-networking-e-nva-bgp/spoke-dev.tf +++ b/fast/stages/2-networking-e-nva-bgp/spoke-dev.tf @@ -45,11 +45,13 @@ module "dev-spoke-project" { } module "dev-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.dev-spoke-project.project_id - name = "dev-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/dev" + source = "../../../modules/net-vpc" + project_id = module.dev-spoke-project.project_id + name = "dev-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/dev" + } delete_default_routes_on_create = true psa_config = try(var.psa_ranges.dev, null) # Set explicit routes for googleapis; send everything else to NVAs diff --git a/fast/stages/2-networking-e-nva-bgp/spoke-prod.tf b/fast/stages/2-networking-e-nva-bgp/spoke-prod.tf index c0ba4414..6ae49dee 100644 --- a/fast/stages/2-networking-e-nva-bgp/spoke-prod.tf +++ b/fast/stages/2-networking-e-nva-bgp/spoke-prod.tf @@ -44,11 +44,13 @@ module "prod-spoke-project" { } module "prod-spoke-vpc" { - source = "../../../modules/net-vpc" - project_id = module.prod-spoke-project.project_id - name = "prod-spoke-0" - mtu = 1500 - data_folder = "${var.factories_config.data_dir}/subnets/prod" + source = "../../../modules/net-vpc" + project_id = module.prod-spoke-project.project_id + name = "prod-spoke-0" + mtu = 1500 + factories_config = { + subnets_folder = "${var.factories_config.data_dir}/subnets/prod" + } delete_default_routes_on_create = true psa_config = try(var.psa_ranges.prod, null) # Set explicit routes for googleapis; send everything else to NVAs diff --git a/fast/stages/2-networking-e-nva-bgp/variables.tf b/fast/stages/2-networking-e-nva-bgp/variables.tf index b8773041..a784fda3 100644 --- a/fast/stages/2-networking-e-nva-bgp/variables.tf +++ b/fast/stages/2-networking-e-nva-bgp/variables.tf @@ -173,18 +173,14 @@ variable "psa_ranges" { description = "IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format." type = object({ dev = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) prod = object({ - ranges = map(string) - routes = object({ - export = bool - import = bool - }) + ranges = map(string) + export_routes = optional(bool, false) + import_routes = optional(bool, false) }) }) default = null diff --git a/fast/stages/2-security/README.md b/fast/stages/2-security/README.md index e28aac7b..9d47bdaf 100644 --- a/fast/stages/2-security/README.md +++ b/fast/stages/2-security/README.md @@ -284,13 +284,12 @@ Some references that might be useful in setting up this stage: - ## Files | name | description | modules | resources | |---|---|---|---| -| [core-dev.tf](./core-dev.tf) | None | kms · project | google_project_iam_member | -| [core-prod.tf](./core-prod.tf) | None | kms · project | google_project_iam_member | +| [core-dev.tf](./core-dev.tf) | None | kms · project | | +| [core-prod.tf](./core-prod.tf) | None | kms · project | | | [main.tf](./main.tf) | Module-level locals and resources. | | | | [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object · local_file | | [variables.tf](./variables.tf) | Module variables. | | | @@ -303,17 +302,16 @@ Some references that might be useful in setting up this stage: | [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [billing_account](variables.tf#L25) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | | [folder_ids](variables.tf#L38) | Folder name => id mappings, the 'security' folder name must exist. | object({…}) | ✓ | | 1-resman | -| [organization](variables.tf#L84) | Organization details. | object({…}) | ✓ | | 0-bootstrap | -| [prefix](variables.tf#L100) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | -| [service_accounts](variables.tf#L111) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L97) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L113) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [service_accounts](variables.tf#L124) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…}) | ✓ | | 1-resman | | [groups](variables.tf#L46) | Group names to grant organization-level permissions. | map(string) | | {…} | 0-bootstrap | -| [kms_defaults](variables.tf#L61) | Defaults used for KMS keys. | object({…}) | | {…} | | -| [kms_keys](variables.tf#L73) | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | map(object({…})) | | {} | | -| [outputs_location](variables.tf#L94) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [vpc_sc_access_levels](variables.tf#L122) | VPC SC access level definitions. | map(object({…})) | | {} | | -| [vpc_sc_egress_policies](variables.tf#L151) | VPC SC egress policy definitions. | map(object({…})) | | {} | | -| [vpc_sc_ingress_policies](variables.tf#L171) | VPC SC ingress policy definitions. | map(object({…})) | | {} | | -| [vpc_sc_perimeters](variables.tf#L192) | VPC SC regular perimeter definitions. | object({…}) | | {} | | +| [kms_keys](variables.tf#L61) | KMS keys to create, keyed by name. | map(object({…})) | | {} | | +| [outputs_location](variables.tf#L107) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [vpc_sc_access_levels](variables.tf#L135) | VPC SC access level definitions. | map(object({…})) | | {} | | +| [vpc_sc_egress_policies](variables.tf#L164) | VPC SC egress policy definitions. | map(object({…})) | | {} | | +| [vpc_sc_ingress_policies](variables.tf#L184) | VPC SC ingress policy definitions. | map(object({…})) | | {} | | +| [vpc_sc_perimeters](variables.tf#L205) | VPC SC regular perimeter definitions. | object({…}) | | {} | | ## Outputs @@ -322,5 +320,4 @@ Some references that might be useful in setting up this stage: | [kms_keys](outputs.tf#L59) | KMS key ids. | | | | [stage_perimeter_projects](outputs.tf#L64) | Security project numbers. They can be added to perimeter resources. | | | | [tfvars](outputs.tf#L74) | Terraform variable files for the following stages. | ✓ | | - diff --git a/fast/stages/2-security/core-dev.tf b/fast/stages/2-security/core-dev.tf index 1b494947..6f71318d 100644 --- a/fast/stages/2-security/core-dev.tf +++ b/fast/stages/2-security/core-dev.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. @@ -16,11 +16,11 @@ locals { dev_kms_restricted_admins = [ - for sa in compact([ + for sa in distinct(compact([ var.service_accounts.data-platform-dev, var.service_accounts.project-factory-dev, var.service_accounts.project-factory-prod - ]) : "serviceAccount:${sa}" + ])) : "serviceAccount:${sa}" ] } @@ -33,6 +33,12 @@ module "dev-sec-project" { iam = { "roles/cloudkms.viewer" = local.dev_kms_restricted_admins } + iam_bindings_additive = { + for member in local.dev_kms_restricted_admins : + "kms_restricted_admin.${member}" => merge(local.kms_restricted_admin_template, { + member = member + }) + } labels = { environment = "dev", team = "security" } services = local.project_services } @@ -45,30 +51,5 @@ module "dev-sec-kms" { location = each.key name = "dev-${each.key}" } - # rename to `key_iam` to switch to authoritative bindings - key_iam = { - for k, v in local.kms_locations_keys[each.key] : k => v.iam - } keys = local.kms_locations_keys[each.key] } - -# TODO(ludo): add support for conditions to Fabric modules - -resource "google_project_iam_member" "dev_key_admin_delegated" { - for_each = toset(local.dev_kms_restricted_admins) - project = module.dev-sec-project.project_id - role = "roles/cloudkms.admin" - member = each.key - condition { - title = "kms_sa_delegated_grants" - description = "Automation service account delegated grants." - expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && resource.type == 'cloudkms.googleapis.com/CryptoKey'", - join(",", formatlist("'%s'", [ - "roles/cloudkms.cryptoKeyEncrypterDecrypter", - "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation" - ])) - ) - } - depends_on = [module.dev-sec-project] -} diff --git a/fast/stages/2-security/core-prod.tf b/fast/stages/2-security/core-prod.tf index 559ff32f..1d536249 100644 --- a/fast/stages/2-security/core-prod.tf +++ b/fast/stages/2-security/core-prod.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. @@ -16,10 +16,10 @@ locals { prod_kms_restricted_admins = [ - for sa in compact([ + for sa in distinct(compact([ var.service_accounts.data-platform-prod, var.service_accounts.project-factory-prod - ]) : "serviceAccount:${sa}" + ])) : "serviceAccount:${sa}" ] } @@ -32,6 +32,12 @@ module "prod-sec-project" { iam = { "roles/cloudkms.viewer" = local.prod_kms_restricted_admins } + iam_bindings_additive = { + for member in local.prod_kms_restricted_admins : + "kms_restricted_admin.${member}" => merge(local.kms_restricted_admin_template, { + member = member + }) + } labels = { environment = "prod", team = "security" } services = local.project_services } @@ -44,30 +50,5 @@ module "prod-sec-kms" { location = each.key name = "prod-${each.key}" } - # rename to `key_iam` to switch to authoritative bindings - key_iam = { - for k, v in local.kms_locations_keys[each.key] : k => v.iam - } keys = local.kms_locations_keys[each.key] } - -# TODO(ludo): add support for conditions to Fabric modules - -resource "google_project_iam_member" "prod_key_admin_delegated" { - for_each = toset(local.prod_kms_restricted_admins) - project = module.prod-sec-project.project_id - role = "roles/cloudkms.admin" - member = each.key - condition { - title = "kms_sa_delegated_grants" - description = "Automation service account delegated grants." - expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && resource.type == 'cloudkms.googleapis.com/CryptoKey'", - join(",", formatlist("'%s'", [ - "roles/cloudkms.cryptoKeyEncrypterDecrypter", - "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation" - ])) - ) - } - depends_on = [module.prod-sec-project] -} diff --git a/fast/stages/2-security/main.tf b/fast/stages/2-security/main.tf index 13078d12..70799011 100644 --- a/fast/stages/2-security/main.tf +++ b/fast/stages/2-security/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. @@ -15,28 +15,36 @@ */ locals { - kms_keys = { - for k, v in var.kms_keys : k => { - iam = coalesce(v.iam, {}) - labels = coalesce(v.labels, {}) - locations = ( - v.locations == null - ? var.kms_defaults.locations - : v.locations - ) - rotation_period = ( - v.rotation_period == null - ? var.kms_defaults.rotation_period - : v.rotation_period + # additive IAM binding for delegated KMS admins + kms_restricted_admin_template = { + role = "roles/cloudkms.admin" + condition = { + title = "kms_sa_delegated_grants" + description = "Automation service account delegated grants." + expression = format( + <<-EOT + api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && + resource.type == 'cloudkms.googleapis.com/CryptoKey' + EOT + , join(",", formatlist("'%s'", [ + "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation" + ])) ) } } + + # list of locations with keys kms_locations = distinct(flatten([ - for k, v in local.kms_keys : v.locations + for k, v in var.kms_keys : v.locations ])) + # map { location -> { key_name -> key_details } } kms_locations_keys = { - for loc in local.kms_locations : loc => { - for k, v in local.kms_keys : k => v if contains(v.locations, loc) + for loc in local.kms_locations : + loc => { + for k, v in var.kms_keys : + k => v + if contains(v.locations, loc) } } project_services = [ diff --git a/fast/stages/2-security/variables.tf b/fast/stages/2-security/variables.tf index f798de78..fa439c8c 100644 --- a/fast/stages/2-security/variables.tf +++ b/fast/stages/2-security/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. @@ -58,27 +58,40 @@ variable "groups" { } } -variable "kms_defaults" { - description = "Defaults used for KMS keys." - type = object({ - locations = list(string) - rotation_period = string - }) - default = { - locations = ["europe", "europe-west1", "europe-west3", "global"] - rotation_period = "7776000s" - } -} - variable "kms_keys" { - description = "KMS keys to create, keyed by name. Null attributes will be interpolated with defaults." + description = "KMS keys to create, keyed by name." type = map(object({ - iam = map(list(string)) - labels = map(string) - locations = list(string) - rotation_period = string + rotation_period = optional(string, "7776000s") + labels = optional(map(string)) + locations = optional(list(string), ["europe", "europe-west1", "europe-west3", "global"]) + purpose = optional(string, "ENCRYPT_DECRYPT") + skip_initial_version_creation = optional(bool, false) + version_template = optional(object({ + algorithm = string + protection_level = optional(string, "SOFTWARE") + })) + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })) - default = {} + default = {} + nullable = false } variable "organization" { diff --git a/fast/stages/3-gke-multitenant/dev/README.md b/fast/stages/3-gke-multitenant/dev/README.md index 23572297..f099c10b 100644 --- a/fast/stages/3-gke-multitenant/dev/README.md +++ b/fast/stages/3-gke-multitenant/dev/README.md @@ -163,21 +163,21 @@ Leave all these variables unset (or set to `null`) to disable fleet management. |---|---|:---:|:---:|:---:|:---:| | [automation](variables.tf#L21) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | | [billing_account](variables.tf#L29) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | -| [folder_ids](variables.tf#L159) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | -| [host_project_ids](variables.tf#L174) | Host project for the shared VPC. | object({…}) | ✓ | | 2-networking | -| [prefix](variables.tf#L227) | Prefix used for resources that need unique names. | string | ✓ | | | -| [vpc_self_links](variables.tf#L243) | Self link for the shared VPC. | object({…}) | ✓ | | 2-networking | -| [clusters](variables.tf#L42) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | | -| [fleet_configmanagement_clusters](variables.tf#L96) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | -| [fleet_configmanagement_templates](variables.tf#L104) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | -| [fleet_features](variables.tf#L139) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | -| [fleet_workload_identity](variables.tf#L152) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | -| [group_iam](variables.tf#L167) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | | -| [iam](variables.tf#L182) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | -| [labels](variables.tf#L189) | Project-level labels. | map(string) | | {} | | -| [nodepools](variables.tf#L195) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | -| [outputs_location](variables.tf#L221) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L236) | Additional project services to enable. | list(string) | | [] | | +| [folder_ids](variables.tf#L174) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [host_project_ids](variables.tf#L189) | Host project for the shared VPC. | object({…}) | ✓ | | 2-networking | +| [prefix](variables.tf#L242) | Prefix used for resources that need unique names. | string | ✓ | | | +| [vpc_self_links](variables.tf#L258) | Self link for the shared VPC. | object({…}) | ✓ | | 2-networking | +| [clusters](variables.tf#L42) | Clusters configuration. Refer to the gke-cluster-standard module for type details. | map(object({…})) | | {} | | +| [fleet_configmanagement_clusters](variables.tf#L111) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | +| [fleet_configmanagement_templates](variables.tf#L119) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | +| [fleet_features](variables.tf#L154) | Enable and configure fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | +| [fleet_workload_identity](variables.tf#L167) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | +| [group_iam](variables.tf#L182) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | | +| [iam](variables.tf#L197) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | +| [labels](variables.tf#L204) | Project-level labels. | map(string) | | {} | | +| [nodepools](variables.tf#L210) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | +| [outputs_location](variables.tf#L236) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L251) | Additional project services to enable. | list(string) | | [] | | ## Outputs diff --git a/fast/stages/3-gke-multitenant/dev/variables.tf b/fast/stages/3-gke-multitenant/dev/variables.tf index 11e32ed6..831f828b 100644 --- a/fast/stages/3-gke-multitenant/dev/variables.tf +++ b/fast/stages/3-gke-multitenant/dev/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. @@ -40,7 +40,7 @@ variable "billing_account" { } variable "clusters" { - description = "Clusters configuration. Refer to the gke-cluster module for type details." + description = "Clusters configuration. Refer to the gke-cluster-standard module for type details." type = map(object({ cluster_autoscaling = optional(any) description = optional(string) @@ -68,9 +68,24 @@ variable "clusters" { max_pods_per_node = optional(number, 110) min_master_version = optional(string) monitoring_config = optional(object({ - enable_components = optional(list(string), ["SYSTEM_COMPONENTS"]) - managed_prometheus = optional(bool) - })) + enable_system_metrics = optional(bool, true) + + # (Optional) control plane metrics + enable_api_server_metrics = optional(bool, false) + enable_controller_manager_metrics = optional(bool, false) + enable_scheduler_metrics = optional(bool, false) + + # (Optional) kube state metrics + enable_daemonset_metrics = optional(bool, false) + enable_deployment_metrics = optional(bool, false) + enable_hpa_metrics = optional(bool, false) + enable_pod_metrics = optional(bool, false) + enable_statefulset_metrics = optional(bool, false) + enable_storage_metrics = optional(bool, false) + + # Google Cloud Managed Service for Prometheus + enable_managed_prometheus = optional(bool, true) + }), {}) node_locations = optional(list(string)) private_cluster_config = optional(any) release_channel = optional(string) @@ -82,9 +97,9 @@ variable "clusters" { services = string })) secondary_range_names = optional(object({ - pods = string - services = string - }), { pods = "pods", services = "services" }) + pods = optional(string, "pods") + services = optional(string, "services") + })) master_authorized_ranges = optional(map(string)) master_ipv4_cidr_block = optional(string) }) diff --git a/fast/stages/3-project-factory/dev/README.md b/fast/stages/3-project-factory/dev/README.md index 2073e759..4c1fe75d 100644 --- a/fast/stages/3-project-factory/dev/README.md +++ b/fast/stages/3-project-factory/dev/README.md @@ -55,7 +55,7 @@ gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-security.auto. If you're not using FAST, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning. -Besides the values above, the project factory is drive by data files, with one file per project. +Besides the values above, the project factory is driven by data files which closely follow the variables exposed by the [project module](../../../../modules/project/), with one file per project. Please refer to the underlying [project factory blueprint](../../../../blueprints/factories/project-factory/) documentation for details on the format. Once the configuration is complete, run the project factory with: @@ -79,8 +79,8 @@ terraform apply | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| | [billing_account](variables.tf#L19) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | -| [factory_data](variables.tf#L32) | Project data from either YAML files or externally parsed data. | object({…}) | ✓ | | | -| [prefix](variables.tf#L48) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L51) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [factory_data](variables.tf#L32) | Project data from either YAML files or externally parsed data. | object({…}) | | {…} | | ## Outputs diff --git a/fast/stages/3-project-factory/dev/data/defaults.yaml b/fast/stages/3-project-factory/dev/data/defaults.yaml deleted file mode 100644 index e52bb132..00000000 --- a/fast/stages/3-project-factory/dev/data/defaults.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# skip boilerplate check - -billing_account_id: 012345-67890A-BCDEF0 - -# [opt] Setup for billing alerts -billing_alert: - amount: 1000 - thresholds: - current: [0.5, 0.8] - forecasted: [0.5, 0.8] - credit_treatment: INCLUDE_ALL_CREDITS - -# [opt] Contacts for billing alerts and important notifications -essential_contacts: ["team-contacts@example.com"] - -# [opt] Labels set for all projects -labels: - environment: dev - department: accounting - application: example-app - foo: bar - -# [opt] Additional notification channels for billing -notification_channels: [] diff --git a/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample b/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample deleted file mode 100644 index 5311019d..00000000 --- a/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample +++ /dev/null @@ -1,103 +0,0 @@ -# skip boilerplate check - -# [opt] Billing account id - overrides default if set -billing_account_id: 012345-67890A-BCDEF0 - -# [opt] Billing alerts config - overrides default if set -billing_alert: - amount: 10 - thresholds: - current: - - 0.5 - - 0.8 - forecasted: [] - credit_treatment: INCLUDE_ALL_CREDITS - -# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults -dns_zones: - - lorem - - ipsum - -# [opt] Contacts for billing alerts and important notifications -essential_contacts: - - team-a-contacts@example.com - -# Folder the project will be created as children of -folder_id: folders/012345678901 - -# [opt] Authoritative IAM bindings in group => [roles] format -group_iam: - test-team-foobar@fast-lab-0.gcp-pso-italy.net: - - roles/compute.admin - -# [opt] Authoritative IAM bindings in role => [principals] format -# Generally used to grant roles to service accounts external to the project -iam: - roles/compute.admin: - - serviceAccount:service-account - -# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter -# in service => [keys] format -kms_service_agents: - compute: [key1, key2] - storage: [key1, key2] - -# [opt] Labels for the project - merged with the ones defined in defaults -labels: - environment: dev - -# [opt] Org policy overrides defined at project level -org_policies: - compute.disableGuestAttributesAccess: - rules: - - enforce: true - compute.trustedImageProjects: - rules: - - allow: - values: - - projects/fast-dev-iac-core-0 - compute.vmExternalIpAccess: - rules: - - deny: - all: true - -# [opt] Service account to create for the project and their roles on the project -# in name => [roles] format -service_accounts: - another-service-account: - - roles/compute.admin - my-service-account: - - roles/compute.admin - -# [opt] APIs to enable on the project. -services: - - storage.googleapis.com - - stackdriver.googleapis.com - - compute.googleapis.com - -# [opt] Roles to assign to the service identities in service => [roles] format -service_identities_iam: - compute: - - roles/storage.objectViewer - - # [opt] VPC setup. - # If set enables the `compute.googleapis.com` service and configures - # service project attachment -vpc: - # [opt] If set, enables the container API - gke_setup: - # Grants "roles/container.hostServiceAgentUser" to the container robot if set - enable_host_service_agent: false - - # Grants "roles/compute.securityAdmin" to the container robot if set - enable_security_admin: true - - # Host project the project will be service project of - host_project: fast-dev-net-spoke-0 - - # [opt] Subnets in the host project where principals will be granted networkUser - # in region/subnet-name => [principals] - subnets_iam: - europe-west1/dev-default-ew1: - - user:foobar@example.com - - serviceAccount:service-account1 diff --git a/fast/stages/3-project-factory/dev/data/projects/test-project.yaml b/fast/stages/3-project-factory/dev/data/projects/test-project.yaml new file mode 100644 index 00000000..dfe34e6c --- /dev/null +++ b/fast/stages/3-project-factory/dev/data/projects/test-project.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. + +labels: + team: team-0 +parent: folders/1234567890 +services: +- compute.googleapis.com +- storage.googleapis.com diff --git a/fast/stages/3-project-factory/dev/main.tf b/fast/stages/3-project-factory/dev/main.tf index 261351ca..4f23b492 100644 --- a/fast/stages/3-project-factory/dev/main.tf +++ b/fast/stages/3-project-factory/dev/main.tf @@ -31,7 +31,7 @@ module "projects" { ] } data_overrides = { - prefix = var.prefix + prefix = "${var.prefix}-dev" } factory_data = var.factory_data } diff --git a/fast/stages/3-project-factory/dev/variables.tf b/fast/stages/3-project-factory/dev/variables.tf index d004aeb8..c7165e3c 100644 --- a/fast/stages/3-project-factory/dev/variables.tf +++ b/fast/stages/3-project-factory/dev/variables.tf @@ -36,6 +36,9 @@ variable "factory_data" { data_path = optional(string) }) nullable = false + default = { + data_path = "data/projects" + } validation { condition = ( (var.factory_data.data != null ? 1 : 0) + @@ -49,7 +52,6 @@ variable "prefix" { # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string - validation { condition = try(length(var.prefix), 0) < 10 error_message = "Use a maximum of 9 characters for prefix." diff --git a/modules/__docs/20230816-iam-refactor.md b/modules/__docs/20230816-iam-refactor.md index 438252ac..46916657 100644 --- a/modules/__docs/20230816-iam-refactor.md +++ b/modules/__docs/20230816-iam-refactor.md @@ -6,6 +6,7 @@ ## Status Implemented in [#1595](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1595). +Authoritative bindings type changed as per [#1622](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/issues/1622). ## Context @@ -39,15 +40,18 @@ The new `iam_bindings` variable will look like this: ```hcl variable "iam_bindings" { - description = "Authoritative IAM bindings with support for conditions, in {ROLE => { members = [], condition = {}}} format." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ - members = list(string) + members = list(string) + role = string condition = optional(object({ expression = string title = string description = optional(string) })) })) + nullable = false + default = {} } ``` @@ -94,8 +98,8 @@ The new variable will closely follow the type of the authoritative `iam_bindings variable "iam_bindings_additive" { description = "Additive IAM bindings with support for conditions, in {KEY => { role = ROLE, members = [], condition = {}}} format." type = map(object({ - member = string - role = string + member = string + role = string condition = optional(object({ expression = string title = string @@ -128,3 +132,213 @@ This brings several advantages over the previous handling of IAM: ### Blueprints A few data blueprints that leverage `iam_additive` have been refactored to use the new variable. This is most notable in data blueprints, where extra files have been added to the more complex examples like data foundations, to abstract IAM bindings in a way similar to what is described above for FAST. + +## Implementation + +The following sections provide a template for IAM-related variables and resources to ensure a consistent implementation of IAM across the repository. Use these code snippets to add IAM support to your module. + +### Top-level module IAM + +Use this template if your module manages a single instance of a given resource (e.g. a KMS keyring). + +```terraform +# variables.tf + +variable "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format. Mutually exclusive with the access_* variables used for basic roles." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_bindings" { + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." + type = map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + default = {} + nullable = false +} + +variable "iam_bindings_additive" { + description = "Keyring individual additive IAM bindings. Keys are arbitrary." + type = map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + default = {} + nullable = false +} +``` + +```terraform +# iam.tf + +resource "google_RESOURCE_TYPE_iam_binding" "authoritative" { + for_each = var.iam + role = each.key + members = each.value + // add extra attributes (e.g. resource id) +} + +resource "google_RESOURCE_TYPE_iam_binding" "bindings" { + for_each = var.iam_bindings + role = each.value.role + members = each.value.members + // add extra attributes (e.g. resource id) + + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_RESOURCE_TYPE_iam_member" "bindings" { + for_each = var.iam_bindings_additive + role = each.value.role + member = each.value.member + // add extra attributes (e.g. resource id) + + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} +``` + +### Sub-resources IAM + +Use this template if your module manages multiple instances of a resource (e.g. keys in KMS keyring). + +```terraform +# variables.tf +variable "sub_resources" { + type = map(object({ + # sub-resource configuration here + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + })) + default = {} + nullable = false +} +``` + +```terraform +# iam.tf +locals { + SUB_RESOURCE_iam = flatten([ + for k, v in var.SUB_RESOURCEs : [ + for role, members in v.iam : { + key = k + role = role + members = members + } + ] + ]) + SUB_RESOURCE_iam_bindings = merge([ + for k, v in var.SUB_RESOURCEs : { + for binding_key, data in v.iam_bindings : + binding_key => { + SUB_RESOURCE = k + role = data.role + members = data.members + condition = data.condition + } + } + ]...) + SUB_RESOURCE_iam_bindings_additive = merge([ + for k, v in var.subresources : { + for binding_key, data in v.iam_bindings_additive : + binding_key => { + SUB_RESOURCE = k + role = data.role + member = data.member + condition = data.condition + } + } + ]...) +} +``` + +```terraform +# iam.tf + +resource "google_SUB_RESOURCE_iam_binding" "authoritative" { + for_each = { + for binding in local.SUB_RESOURCE_iam : + "${binding.key}.${binding.role}" => binding + } + role = each.value.role + members = each.value.members + // add extra attributes (e.g. sub resource id) +} + +resource "google_SUB_RESOURCE_iam_binding" "bindings" { + for_each = local.SUB_RESOURCE_iam_bindings + role = each.value.role + members = each.value.members + // add extra attributes (e.g. sub resource id) + + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_SUB_RESOURCE_iam_member" "members" { + for_each = local.SUB_RESOURCE_iam_bindings_additive + role = each.value.role + member = each.value.member + // add extra attributes (e.g. sub resource id) + + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +``` diff --git a/modules/__experimental/net-neg/versions.tf b/modules/__experimental/net-neg/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/__experimental/net-neg/versions.tf +++ b/modules/__experimental/net-neg/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/alloydb-instance/versions.tf b/modules/alloydb-instance/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/alloydb-instance/versions.tf +++ b/modules/alloydb-instance/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/api-gateway/versions.tf b/modules/api-gateway/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/api-gateway/versions.tf +++ b/modules/api-gateway/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/apigee/README.md b/modules/apigee/README.md index 67daa729..cb99a34a 100644 --- a/modules/apigee/README.md +++ b/modules/apigee/README.md @@ -2,7 +2,121 @@ This module simplifies the creation of a Apigee resources (organization, environment groups, environment group attachments, environments, instances and instance attachments). -## Example +## Examples + + +- [Examples](#examples) + - [Minimal example (CLOUD)](#minimal-example-cloud) + - [Minimal example with existing organization (CLOUD)](#minimal-example-with-existing-organization-cloud) + - [Disable VPC Peering (CLOUD)](#disable-vpc-peering-cloud) + - [All resources (CLOUD)](#all-resources-cloud) + - [All resources (HYBRID control plane)](#all-resources-hybrid-control-plane) + - [New environment group](#new-environment-group) + - [New environment](#new-environment) + - [New instance (VPC Peering Provisioning Mode)](#new-instance-vpc-peering-provisioning-mode) + - [New instance (Non VPC Peering Provisioning Mode)](#new-instance-non-vpc-peering-provisioning-mode) + - [New endpoint attachment](#new-endpoint-attachment) + - [Apigee add-ons](#apigee-add-ons) +- [Variables](#variables) +- [Outputs](#outputs) + + +### Minimal example (CLOUD) + +This example shows how to create to create an Apigee organization and deploy instance in it. + +```hcl +module "apigee" { + source = "./fabric/modules/apigee" + project_id = var.project_id + organization = { + display_name = "Apigee" + billing_type = "PAYG" + analytics_region = "europe-west1" + authorized_network = var.vpc.id + runtime_type = "CLOUD" + } + envgroups = { + prod = ["prod.example.com"] + } + environments = { + apis-prod = { + display_name = "APIs prod" + description = "APIs Prod" + envgroups = ["prod"] + } + } + instances = { + europe-west1 = { + environments = ["apis-prod"] + runtime_ip_cidr_range = "10.32.0.0/22" + troubleshooting_ip_cidr_range = "10.64.0.0/28" + } + } +} +# tftest modules=1 resources=6 inventory=minimal-cloud.yaml +``` + +### Minimal example with existing organization (CLOUD) + +This example shows how to create to work with an existing organization in the project. Note that in this case we don't specify the IP ranges for the instance, so it requests and allocates an available /22 and /28 CIDR block from Service Networking to deploy the instance. + +```hcl +module "apigee" { + source = "./fabric/modules/apigee" + project_id = var.project_id + envgroups = { + prod = ["prod.example.com"] + } + environments = { + apis-prod = { + display_name = "APIs prod" + envgroups = ["prod"] + } + } + instances = { + europe-west1 = { + environments = ["apis-prod"] + } + } +} +# tftest modules=1 resources=5 inventory=minimal-cloud-no-org.yaml +``` + +### Disable VPC Peering (CLOUD) + +When a new Apigee organization is created, it is automatically peered to the authorized network. You can prevent this from happening by using the `disable_vpc_peering` key in the `organization` variable, as shown below: + + +```hcl +module "apigee" { + source = "./fabric/modules/apigee" + project_id = var.project_id + organization = { + display_name = "Apigee" + billing_type = "PAYG" + analytics_region = "europe-west1" + runtime_type = "CLOUD" + disable_vpc_peering = true + } + envgroups = { + prod = ["prod.example.com"] + } + environments = { + apis-prod = { + display_name = "APIs prod" + envgroups = ["prod"] + } + } + instances = { + europe-west1 = { + environments = ["apis-prod"] + } + } +} +# tftest modules=1 resources=6 inventory=no-peering.yaml +``` + ### All resources (CLOUD) @@ -28,13 +142,11 @@ module "apigee" { display_name = "APIs test" description = "APIs Test" envgroups = ["test"] - regions = ["europe-west1"] } apis-prod = { display_name = "APIs prod" description = "APIs prod" envgroups = ["prod"] - regions = ["europe-west3"] iam = { "roles/viewer" = ["group:devops@myorg.com"] } @@ -44,10 +156,12 @@ module "apigee" { europe-west1 = { runtime_ip_cidr_range = "10.0.4.0/22" troubleshooting_ip_cidr_range = "10.1.1.0.0/28" + environments = ["apis-test"] } europe-west3 = { runtime_ip_cidr_range = "10.0.8.0/22" troubleshooting_ip_cidr_range = "10.1.16.0/28" + environments = ["apis-prod"] enable_nat = true } } @@ -129,7 +243,7 @@ module "apigee" { # tftest modules=1 resources=1 ``` -### New instance +### New instance (VPC Peering Provisioning Mode) ```hcl module "apigee" { @@ -145,6 +259,28 @@ module "apigee" { # tftest modules=1 resources=1 ``` +### New instance (Non VPC Peering Provisioning Mode) + +```hcl +module "apigee" { + source = "./fabric/modules/apigee" + project_id = "my-project" + organization = { + display_name = "My Organization" + description = "My Organization" + runtime_type = "CLOUD" + billing_type = "Pay-as-you-go" + database_encryption_key = "123456789" + analytics_region = "europe-west1" + disable_vpc_peering = true + } + instances = { + europe-west1 = {} + } +} +# tftest modules=1 resources=2 +``` + ### New endpoint attachment Endpoint attachments allow to implement [Apigee southbound network patterns](https://cloud.google.com/apigee/docs/api-platform/architecture/southbound-networking-patterns-endpoints#create-the-psc-attachments). @@ -180,6 +316,7 @@ module "apigee" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| +<<<<<<< HEAD | [project_id](variables.tf#L97) | Project ID. | string | ✓ | | | [addons_config](variables.tf#L17) | Addons configuration. | object({…}) | | null | | [endpoint_attachments](variables.tf#L29) | Endpoint attachments. | map(object({…})) | | {} | @@ -187,6 +324,15 @@ module "apigee" { | [environments](variables.tf#L46) | Environments. | map(object({…})) | | {} | | [instances](variables.tf#L65) | Instances ([REGION] => [INSTANCE]). | map(object({…})) | | {} | | [organization](variables.tf#L82) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | +======= +| [project_id](variables.tf#L117) | Project ID. | string | ✓ | | +| [addons_config](variables.tf#L17) | Addons configuration. | object({…}) | | null | +| [endpoint_attachments](variables.tf#L29) | Endpoint attachments. | map(object({…})) | | {} | +| [envgroups](variables.tf#L39) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | | {} | +| [environments](variables.tf#L46) | Environments. | map(object({…})) | | {} | +| [instances](variables.tf#L64) | Instances ([REGION] => [INSTANCE]). | map(object({…})) | | {} | +| [organization](variables.tf#L89) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | +>>>>>>> master ## Outputs diff --git a/modules/apigee/main.tf b/modules/apigee/main.tf index 84c121db..cd1f7197 100644 --- a/modules/apigee/main.tf +++ b/modules/apigee/main.tf @@ -28,6 +28,7 @@ resource "google_apigee_organization" "organization" { runtime_type = var.organization.runtime_type runtime_database_encryption_key_name = var.organization.database_encryption_key retention = var.organization.retention + disable_vpc_peering = var.organization.disable_vpc_peering } resource "google_apigee_envgroup" "envgroups" { @@ -85,13 +86,17 @@ resource "google_apigee_environment_iam_binding" "binding" { } resource "google_apigee_instance" "instances" { - for_each = var.instances - name = coalesce(each.value.name, "instance-${each.key}") - display_name = each.value.display_name - description = each.value.description - location = each.key - org_id = local.org_id - ip_range = "${each.value.runtime_ip_cidr_range},${each.value.troubleshooting_ip_cidr_range}" + for_each = var.instances + name = coalesce(each.value.name, "instance-${each.key}") + display_name = each.value.display_name + description = each.value.description + location = each.key + org_id = local.org_id + ip_range = ( + compact([each.value.runtime_ip_cidr_range, each.value.troubleshooting_ip_cidr_range]) == [] + ? null + : join(",", compact([each.value.runtime_ip_cidr_range, each.value.troubleshooting_ip_cidr_range])) + ) disk_encryption_key_name = each.value.disk_encryption_key consumer_accept_list = each.value.consumer_accept_list } @@ -109,12 +114,12 @@ resource "google_apigee_nat_address" "apigee_nat" { resource "google_apigee_instance_attachment" "instance_attachments" { for_each = merge(concat([for k1, v1 in var.instances : { for v2 in coalesce(v1.environments, []) : - "${k1}-${v2}" => { + "${v2}-${k1}" => { instance = k1 environment = v2 } }])...) - instance_id = google_apigee_instance.instances[each.value.region].id + instance_id = google_apigee_instance.instances[each.value.instance].id environment = try(google_apigee_environment.environments[each.value.environment].name, "${local.org_id}/environments/${each.value.environment}") } @@ -127,7 +132,7 @@ resource "google_apigee_endpoint_attachment" "endpoint_attachments" { service_attachment = each.value.service_attachment } -resource "google_apigee_addons_config" "test_organization" { +resource "google_apigee_addons_config" "addons_config" { for_each = toset(var.addons_config == null ? [] : [""]) org = local.org_name addons_config { diff --git a/modules/apigee/variables.tf b/modules/apigee/variables.tf index db09c28c..3109956d 100644 --- a/modules/apigee/variables.tf +++ b/modules/apigee/variables.tf @@ -64,16 +64,26 @@ variable "environments" { variable "instances" { description = "Instances ([REGION] => [INSTANCE])." type = map(object({ + name = optional(string) display_name = optional(string) name = optional(string) description = optional(string, "Terraform-managed") - runtime_ip_cidr_range = string - troubleshooting_ip_cidr_range = string + runtime_ip_cidr_range = optional(string) + troubleshooting_ip_cidr_range = optional(string) disk_encryption_key = optional(string) consumer_accept_list = optional(list(string)) environments = optional(list(string)) enable_nat = optional(bool, false) + environments = optional(list(string)) })) + validation { + condition = alltrue([ + for k, v in var.instances : + # has troubleshooting_ip => has runtime_ip + v.runtime_ip_cidr_range != null || v.troubleshooting_ip_cidr_range == null + ]) + error_message = "Using a troubleshooting range requires specifying a runtime range too." + } default = {} nullable = false } @@ -89,7 +99,20 @@ variable "organization" { database_encryption_key = optional(string) analytics_region = optional(string, "europe-west1") retention = optional(string) + disable_vpc_peering = optional(bool, false) }) + validation { + condition = var.organization == null || ( + try(var.organization.runtime_type, null) == "CLOUD" || !try(var.organization.disable_vpc_peering, false) + ) + error_message = "Disabling the VPC peering can only be done in organization using the CLOUD runtime." + } + validation { + condition = var.organization == null || ( + try(var.organization.authorized_network, null) == null || !try(var.organization.disable_vpc_peering, false) + ) + error_message = "Disabling the VPC peering is mutually exclusive with authorized_network." + } default = null } diff --git a/modules/apigee/versions.tf b/modules/apigee/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/apigee/versions.tf +++ b/modules/apigee/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/artifact-registry/versions.tf b/modules/artifact-registry/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/artifact-registry/versions.tf +++ b/modules/artifact-registry/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/bigquery-dataset/versions.tf b/modules/bigquery-dataset/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/bigquery-dataset/versions.tf +++ b/modules/bigquery-dataset/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/bigtable-instance/versions.tf b/modules/bigtable-instance/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/bigtable-instance/versions.tf +++ b/modules/bigtable-instance/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/billing-budget/versions.tf b/modules/billing-budget/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/billing-budget/versions.tf +++ b/modules/billing-budget/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/binauthz/versions.tf b/modules/binauthz/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/binauthz/versions.tf +++ b/modules/binauthz/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/__need_fixing/onprem/versions.tf b/modules/cloud-config-container/__need_fixing/onprem/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/__need_fixing/onprem/versions.tf +++ b/modules/cloud-config-container/__need_fixing/onprem/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/coredns/versions.tf b/modules/cloud-config-container/coredns/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/coredns/versions.tf +++ b/modules/cloud-config-container/coredns/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/cos-generic-metadata/versions.tf b/modules/cloud-config-container/cos-generic-metadata/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/cos-generic-metadata/versions.tf +++ b/modules/cloud-config-container/cos-generic-metadata/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/envoy-traffic-director/versions.tf b/modules/cloud-config-container/envoy-traffic-director/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/envoy-traffic-director/versions.tf +++ b/modules/cloud-config-container/envoy-traffic-director/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/mysql/versions.tf b/modules/cloud-config-container/mysql/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/mysql/versions.tf +++ b/modules/cloud-config-container/mysql/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/nginx-tls/versions.tf b/modules/cloud-config-container/nginx-tls/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/nginx-tls/versions.tf +++ b/modules/cloud-config-container/nginx-tls/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/nginx/versions.tf b/modules/cloud-config-container/nginx/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/nginx/versions.tf +++ b/modules/cloud-config-container/nginx/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/simple-nva/versions.tf b/modules/cloud-config-container/simple-nva/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/simple-nva/versions.tf +++ b/modules/cloud-config-container/simple-nva/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-config-container/squid/versions.tf b/modules/cloud-config-container/squid/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-config-container/squid/versions.tf +++ b/modules/cloud-config-container/squid/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-function-v1/versions.tf b/modules/cloud-function-v1/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-function-v1/versions.tf +++ b/modules/cloud-function-v1/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-function-v2/versions.tf b/modules/cloud-function-v2/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-function-v2/versions.tf +++ b/modules/cloud-function-v2/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-identity-group/versions.tf b/modules/cloud-identity-group/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-identity-group/versions.tf +++ b/modules/cloud-identity-group/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloud-run/versions.tf b/modules/cloud-run/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloud-run/versions.tf +++ b/modules/cloud-run/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/cloudsql-instance/README.md b/modules/cloudsql-instance/README.md index 00cf5ded..74afa419 100644 --- a/modules/cloudsql-instance/README.md +++ b/modules/cloudsql-instance/README.md @@ -116,13 +116,12 @@ module "kms" { location = var.region } keys = { - key-sql = null - } - key_iam = { key-sql = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - "serviceAccount:${module.project.service_accounts.robots.sqladmin}" - ] + iam = { + "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ + "serviceAccount:${module.project.service_accounts.robots.sqladmin}" + ] + } } } } diff --git a/modules/cloudsql-instance/versions.tf b/modules/cloudsql-instance/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/cloudsql-instance/versions.tf +++ b/modules/cloudsql-instance/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md index b281c2e3..5e3dbd8e 100644 --- a/modules/compute-mig/README.md +++ b/modules/compute-mig/README.md @@ -389,7 +389,6 @@ module "nginx-mig" { # tftest modules=2 resources=3 inventory=stateful.yaml ``` - ## Variables | name | description | type | required | default | @@ -400,7 +399,7 @@ module "nginx-mig" { | [project_id](variables.tf#L198) | Project id. | string | ✓ | | | [all_instances_config](variables.tf#L17) | Metadata and labels set to all instances in the group. | object({…}) | | null | | [auto_healing_policies](variables.tf#L26) | Auto-healing policies for this group. | object({…}) | | null | -| [autoscaler_config](variables.tf#L35) | Optional autoscaler configuration. | object({…}) | | null | +| [autoscaler_config](variables.tf#L35) | Optional autoscaler configuration. | object({…}) | | null | | [default_version_name](variables.tf#L83) | Name used for the default version. | string | | "default" | | [description](variables.tf#L89) | Optional description used for all resources managed by this module. | string | | "Terraform managed." | | [distribution_policy](variables.tf#L95) | DIstribution policy for regional MIG. | object({…}) | | null | @@ -422,5 +421,4 @@ module "nginx-mig" { | [group_manager](outputs.tf#L26) | Instance group resource. | | | [health_check](outputs.tf#L35) | Auto-created health-check resource. | | | [id](outputs.tf#L44) | Fully qualified group manager id. | | - diff --git a/modules/compute-mig/autoscaler.tf b/modules/compute-mig/autoscaler.tf index b8bd0acc..c0f77491 100644 --- a/modules/compute-mig/autoscaler.tf +++ b/modules/compute-mig/autoscaler.tf @@ -35,6 +35,7 @@ resource "google_compute_autoscaler" "default" { max_replicas = var.autoscaler_config.max_replicas min_replicas = var.autoscaler_config.min_replicas cooldown_period = var.autoscaler_config.cooldown_period + mode = var.autoscaler_config.mode dynamic "scale_down_control" { for_each = local.as_scaling.down == null ? [] : [""] @@ -138,6 +139,7 @@ resource "google_compute_region_autoscaler" "default" { max_replicas = var.autoscaler_config.max_replicas min_replicas = var.autoscaler_config.min_replicas cooldown_period = var.autoscaler_config.cooldown_period + mode = var.autoscaler_config.mode dynamic "scale_down_control" { for_each = local.as_scaling.down == null ? [] : [""] diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf index 30f2ce96..20864d18 100644 --- a/modules/compute-mig/variables.tf +++ b/modules/compute-mig/variables.tf @@ -61,8 +61,8 @@ variable "autoscaler_config" { })) metrics = optional(list(object({ name = string - type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE - target_value = number + type = optional(string) # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE + target_value = optional(number) single_instance_assignment = optional(number) time_series_filter = optional(string) }))) diff --git a/modules/compute-mig/versions.tf b/modules/compute-mig/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/compute-mig/versions.tf +++ b/modules/compute-mig/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/compute-vm/main.tf b/modules/compute-vm/main.tf index 0ca16257..e79cc18c 100644 --- a/modules/compute-vm/main.tf +++ b/modules/compute-vm/main.tf @@ -187,7 +187,7 @@ resource "google_compute_instance" "default" { source = ( config.value.source_type == "attach" ? config.value.source - : google_compute_region_disk.disks[config.key].name + : google_compute_region_disk.disks[config.key].id ) } } diff --git a/modules/compute-vm/versions.tf b/modules/compute-vm/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/compute-vm/versions.tf +++ b/modules/compute-vm/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/container-registry/versions.tf b/modules/container-registry/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/container-registry/versions.tf +++ b/modules/container-registry/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/data-catalog-policy-tag/README.md b/modules/data-catalog-policy-tag/README.md index b08a9feb..8a464784 100644 --- a/modules/data-catalog-policy-tag/README.md +++ b/modules/data-catalog-policy-tag/README.md @@ -79,17 +79,17 @@ module "cmn-dc" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L76) | Name of this taxonomy. | string | ✓ | | -| [project_id](variables.tf#L91) | GCP project id. | | ✓ | | +| [name](variables.tf#L77) | Name of this taxonomy. | string | ✓ | | +| [project_id](variables.tf#L92) | GCP project id. | | ✓ | | | [activated_policy_types](variables.tf#L17) | A list of policy types that are activated for this taxonomy. | list(string) | | ["FINE_GRAINED_ACCESS_CONTROL"] | | [description](variables.tf#L23) | Description of this taxonomy. | string | | "Taxonomy - Terraform managed" | | [group_iam](variables.tf#L29) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L35) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L41) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L55) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [location](variables.tf#L70) | Data Catalog Taxonomy location. | string | | "eu" | -| [prefix](variables.tf#L81) | Optional prefix used to generate project id and name. | string | | null | -| [tags](variables.tf#L95) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(object({…})) | | {} | +| [iam_bindings](variables.tf#L41) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L56) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [location](variables.tf#L71) | Data Catalog Taxonomy location. | string | | "eu" | +| [prefix](variables.tf#L82) | Optional prefix used to generate project id and name. | string | | null | +| [tags](variables.tf#L96) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(object({…})) | | {} | ## Outputs diff --git a/modules/data-catalog-policy-tag/iam.tf b/modules/data-catalog-policy-tag/iam.tf index 268c0c58..06c30763 100644 --- a/modules/data-catalog-policy-tag/iam.tf +++ b/modules/data-catalog-policy-tag/iam.tf @@ -53,7 +53,7 @@ resource "google_data_catalog_taxonomy_iam_binding" "bindings" { provider = google-beta for_each = var.iam_bindings taxonomy = google_data_catalog_taxonomy.default.id - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/data-catalog-policy-tag/variables.tf b/modules/data-catalog-policy-tag/variables.tf index b0df313d..0fef9e7b 100644 --- a/modules/data-catalog-policy-tag/variables.tf +++ b/modules/data-catalog-policy-tag/variables.tf @@ -39,9 +39,10 @@ variable "iam" { } variable "iam_bindings" { - description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/data-catalog-policy-tag/versions.tf b/modules/data-catalog-policy-tag/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/data-catalog-policy-tag/versions.tf +++ b/modules/data-catalog-policy-tag/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/datafusion/versions.tf b/modules/datafusion/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/datafusion/versions.tf +++ b/modules/datafusion/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/dataplex-datascan/README.md b/modules/dataplex-datascan/README.md index 1c950184..4116732f 100644 --- a/modules/dataplex-datascan/README.md +++ b/modules/dataplex-datascan/README.md @@ -431,9 +431,9 @@ module "dataplex-datascan" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [data](variables.tf#L17) | The data source for DataScan. The source can be either a Dataplex `entity` or a BigQuery `resource`. | object({…}) | ✓ | | -| [name](variables.tf#L156) | Name of Dataplex Scan. | string | ✓ | | -| [project_id](variables.tf#L167) | The ID of the project where the Dataplex DataScan will be created. | string | ✓ | | -| [region](variables.tf#L172) | Region for the Dataplex DataScan. | string | ✓ | | +| [name](variables.tf#L157) | Name of Dataplex Scan. | string | ✓ | | +| [project_id](variables.tf#L168) | The ID of the project where the Dataplex DataScan will be created. | string | ✓ | | +| [region](variables.tf#L173) | Region for the Dataplex DataScan. | string | ✓ | | | [data_profile_spec](variables.tf#L29) | DataProfileScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataProfileSpec. | object({…}) | | null | | [data_quality_spec](variables.tf#L38) | DataQualityScan related setting. Variable descriptions are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataQualitySpec. | object({…}) | | null | | [data_quality_spec_file](variables.tf#L80) | Path to a YAML file containing DataQualityScan related setting. Input content can use either camelCase or snake_case. Variables description are provided in https://cloud.google.com/dataplex/docs/reference/rest/v1/DataQualitySpec. | object({…}) | | null | @@ -441,11 +441,11 @@ module "dataplex-datascan" { | [execution_schedule](variables.tf#L94) | Schedule DataScan to run periodically based on a cron schedule expression. If not specified, the DataScan is created with `on_demand` schedule, which means it will not run until the user calls `dataScans.run` API. | string | | null | | [group_iam](variables.tf#L100) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L107) | Dataplex DataScan IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L114) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L128) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [incremental_field](variables.tf#L143) | The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table. | string | | null | -| [labels](variables.tf#L149) | Resource labels. | map(string) | | {} | -| [prefix](variables.tf#L161) | Optional prefix used to generate Dataplex DataScan ID. | string | | null | +| [iam_bindings](variables.tf#L114) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L129) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [incremental_field](variables.tf#L144) | The unnested field (of type Date or Timestamp) that contains values which monotonically increase over time. If not specified, a data scan will run for all data in the table. | string | | null | +| [labels](variables.tf#L150) | Resource labels. | map(string) | | {} | +| [prefix](variables.tf#L162) | Optional prefix used to generate Dataplex DataScan ID. | string | | null | ## Outputs diff --git a/modules/dataplex-datascan/iam.tf b/modules/dataplex-datascan/iam.tf index 9a496ff1..9ed59144 100644 --- a/modules/dataplex-datascan/iam.tf +++ b/modules/dataplex-datascan/iam.tf @@ -44,7 +44,7 @@ resource "google_dataplex_datascan_iam_binding" "bindings" { project = google_dataplex_datascan.datascan.project location = google_dataplex_datascan.datascan.location data_scan_id = google_dataplex_datascan.datascan.data_scan_id - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/dataplex-datascan/variables.tf b/modules/dataplex-datascan/variables.tf index 4e6b2bb1..a13cdc55 100644 --- a/modules/dataplex-datascan/variables.tf +++ b/modules/dataplex-datascan/variables.tf @@ -112,9 +112,10 @@ variable "iam" { } variable "iam_bindings" { - description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/dataplex-datascan/versions.tf b/modules/dataplex-datascan/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/dataplex-datascan/versions.tf +++ b/modules/dataplex-datascan/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/dataplex/versions.tf b/modules/dataplex/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/dataplex/versions.tf +++ b/modules/dataplex/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/dataproc/README.md b/modules/dataproc/README.md index aa532671..5cd220cb 100644 --- a/modules/dataproc/README.md +++ b/modules/dataproc/README.md @@ -146,17 +146,17 @@ module "processing-dp-cluster" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L234) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L249) | Project ID. | string | ✓ | | -| [region](variables.tf#L254) | Dataproc region. | string | ✓ | | +| [name](variables.tf#L235) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L250) | Project ID. | string | ✓ | | +| [region](variables.tf#L255) | Dataproc region. | string | ✓ | | | [dataproc_config](variables.tf#L17) | Dataproc cluster config. | object({…}) | | {} | | [group_iam](variables.tf#L185) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L192) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L199) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L213) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [labels](variables.tf#L228) | The resource labels for instance to use to annotate any related underlying resources, such as Compute Engine VMs. | map(string) | | {} | -| [prefix](variables.tf#L239) | Optional prefix used to generate project id and name. | string | | null | -| [service_account](variables.tf#L259) | Service account to set on the Dataproc cluster. | string | | null | +| [iam_bindings](variables.tf#L199) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L214) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [labels](variables.tf#L229) | The resource labels for instance to use to annotate any related underlying resources, such as Compute Engine VMs. | map(string) | | {} | +| [prefix](variables.tf#L240) | Optional prefix used to generate project id and name. | string | | null | +| [service_account](variables.tf#L260) | Service account to set on the Dataproc cluster. | string | | null | ## Outputs diff --git a/modules/dataproc/iam.tf b/modules/dataproc/iam.tf index fba2eca9..ef0428d1 100644 --- a/modules/dataproc/iam.tf +++ b/modules/dataproc/iam.tf @@ -46,7 +46,7 @@ resource "google_dataproc_cluster_iam_binding" "bindings" { project = var.project_id cluster = google_dataproc_cluster.cluster.name region = var.region - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/dataproc/variables.tf b/modules/dataproc/variables.tf index 49f4fa90..8b77c5b9 100644 --- a/modules/dataproc/variables.tf +++ b/modules/dataproc/variables.tf @@ -197,9 +197,10 @@ variable "iam" { } variable "iam_bindings" { - description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/dataproc/versions.tf b/modules/dataproc/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/dataproc/versions.tf +++ b/modules/dataproc/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/dns-response-policy/versions.tf b/modules/dns-response-policy/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/dns-response-policy/versions.tf +++ b/modules/dns-response-policy/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/dns/README.md b/modules/dns/README.md index cdfff0e3..5b293768 100644 --- a/modules/dns/README.md +++ b/modules/dns/README.md @@ -140,17 +140,16 @@ module "public-dns" { # tftest modules=1 resources=4 inventory=public-zone.yaml ``` - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L33) | Zone name, must be unique within the project. | string | ✓ | | -| [project_id](variables.tf#L38) | Project id for the zone. | string | ✓ | | -| [description](variables.tf#L21) | Domain description. | string | | "Terraform managed." | -| [iam](variables.tf#L27) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | null | -| [recordsets](variables.tf#L43) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | -| [zone_config](variables.tf#L78) | DNS zone configuration. | object({…}) | | null | +| [name](variables.tf#L29) | Zone name, must be unique within the project. | string | ✓ | | +| [project_id](variables.tf#L34) | Project id for the zone. | string | ✓ | | +| [description](variables.tf#L17) | Domain description. | string | | "Terraform managed." | +| [iam](variables.tf#L23) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | null | +| [recordsets](variables.tf#L39) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…})) | | {} | +| [zone_config](variables.tf#L74) | DNS zone configuration. | object({…}) | | null | ## Outputs @@ -162,5 +161,4 @@ module "public-dns" { | [name](outputs.tf#L32) | The DNS zone name. | | | [name_servers](outputs.tf#L37) | The DNS zone name servers. | | | [zone](outputs.tf#L42) | DNS zone resource. | | - diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf index 9c2bf545..08395ba0 100644 --- a/modules/dns/variables.tf +++ b/modules/dns/variables.tf @@ -14,10 +14,6 @@ * limitations under the License. */ -############################################################################### -# zone variables # -############################################################################### - variable "description" { description = "Domain description." type = string diff --git a/modules/dns/versions.tf b/modules/dns/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/dns/versions.tf +++ b/modules/dns/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/endpoints/versions.tf b/modules/endpoints/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/endpoints/versions.tf +++ b/modules/endpoints/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/folder/README.md b/modules/folder/README.md index b4f41601..65661210 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -290,17 +290,17 @@ module "folder" { | [folder_create](variables.tf#L33) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | | [group_iam](variables.tf#L39) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L46) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L53) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L67) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [id](variables.tf#L82) | Folder ID in case you use folder_create=false. | string | | null | -| [logging_data_access](variables.tf#L88) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L103) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L110) | Logging sinks to create for the organization. | map(object({…})) | | {} | -| [name](variables.tf#L140) | Folder name. | string | | null | -| [org_policies](variables.tf#L146) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L173) | Path containing org policies in YAML format. | string | | null | -| [parent](variables.tf#L179) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [tag_bindings](variables.tf#L189) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [iam_bindings](variables.tf#L53) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L68) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [id](variables.tf#L83) | Folder ID in case you use folder_create=false. | string | | null | +| [logging_data_access](variables.tf#L89) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables.tf#L104) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L111) | Logging sinks to create for the organization. | map(object({…})) | | {} | +| [name](variables.tf#L141) | Folder name. | string | | null | +| [org_policies](variables.tf#L147) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [org_policies_data_path](variables.tf#L174) | Path containing org policies in YAML format. | string | | null | +| [parent](variables.tf#L180) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [tag_bindings](variables.tf#L190) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/iam.tf b/modules/folder/iam.tf index 976e312c..20025b28 100644 --- a/modules/folder/iam.tf +++ b/modules/folder/iam.tf @@ -42,7 +42,7 @@ resource "google_folder_iam_binding" "authoritative" { resource "google_folder_iam_binding" "bindings" { for_each = var.iam_bindings folder = local.folder.name - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 619ee9c3..86efc215 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -51,9 +51,10 @@ variable "iam" { } variable "iam_bindings" { - description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/folder/versions.tf b/modules/folder/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/folder/versions.tf +++ b/modules/folder/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/gcs/versions.tf b/modules/gcs/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/gcs/versions.tf +++ b/modules/gcs/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/gcve-private-cloud/versions.tf b/modules/gcve-private-cloud/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/gcve-private-cloud/versions.tf +++ b/modules/gcve-private-cloud/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/gke-cluster-autopilot/README.md b/modules/gke-cluster-autopilot/README.md index da639066..b54588c8 100644 --- a/modules/gke-cluster-autopilot/README.md +++ b/modules/gke-cluster-autopilot/README.md @@ -1,10 +1,23 @@ -# GKE cluster Autopilot module +# GKE Autopilot cluster module -This module allows simplified creation and management of GKE Autopilot clusters. Some sensible defaults are set initially, in order to allow less verbose usage for most use cases. +This module offers a way to create and manage Google Kubernetes Engine (GKE) [Autopilot clusters](https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview). With its sensible default settings based on best practices and authors' experience as Google Cloud practitioners, the module accommodates for many common use cases out-of-the-box, without having to rely on verbose configuration. -## Example + +- [Examples](#examples) + - [GKE Autopilot cluster](#gke-autopilot-cluster) + - [Cloud DNS](#cloud-dns) + - [Logging configuration](#logging-configuration) + - [Monitoring configuration](#monitoring-configuration) + - [Backup for GKE](#backup-for-gke) +- [Variables](#variables) +- [Outputs](#outputs) + -### GKE Cluster +## Examples + +### GKE Autopilot cluster + +This example shows how to [create a GKE cluster in Autopilot mode](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-an-autopilot-cluster). ```hcl module "cluster-1" { @@ -37,7 +50,10 @@ module "cluster-1" { ### Cloud DNS -This example shows how to [use Cloud DNS as a Kubernetes DNS provider](https://cloud.google.com/kubernetes-engine/docs/how-to/cloud-dns) for GKE Standard clusters. +> [!WARNING] +> [Cloud DNS is the only DNS provider for Autopilot clusters](https://cloud.google.com/kubernetes-engine/docs/concepts/service-discovery#cloud_dns) running version `1.25.9-gke.400` and later, and version `1.26.4-gke.500` and later. It is [pre-configured](https://cloud.google.com/kubernetes-engine/docs/resources/autopilot-standard-feature-comparison#feature-comparison) for those clusters. The following example *only* applies to Autopilot clusters running *earlier* versions. + +This example shows how to [use Cloud DNS as a Kubernetes DNS provider](https://cloud.google.com/kubernetes-engine/docs/how-to/cloud-dns). ```hcl module "cluster-1" { @@ -48,7 +64,7 @@ module "cluster-1" { vpc_config = { network = var.vpc.self_link subnetwork = var.subnet.self_link - secondary_range_names = { pods = "pods", services = "services" } + secondary_range_names = {} # use default names "pods" and "services" } enable_features = { dns = { @@ -63,11 +79,11 @@ module "cluster-1" { ### Logging configuration -This example shows how to [collect logs for the Kubernetes control plane components](https://cloud.google.com/stackdriver/docs/solutions/gke/installing). The logs for these components are not collected by default. - -> **Note** +> [!NOTE] > System and workload logs collection is pre-configured for Autopilot clusters and cannot be disabled. +This example shows how to [collect logs for the Kubernetes control plane components](https://cloud.google.com/stackdriver/docs/solutions/gke/installing). The logs for these components are not collected by default. + ```hcl module "cluster-1" { source = "./fabric/modules/gke-cluster-autopilot" @@ -75,8 +91,9 @@ module "cluster-1" { name = "cluster-1" location = "europe-west1" vpc_config = { - network = var.vpc.self_link - subnetwork = var.subnet.self_link + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} # use default names "pods" and "services" } logging_config = { enable_api_server_logs = true @@ -89,36 +106,13 @@ module "cluster-1" { ### Monitoring configuration -This example shows how to [configure collection of Kubernetes control plane metrics](https://cloud.google.com/stackdriver/docs/solutions/gke/managing-metrics#enable-control-plane-metrics). The metrics for these components are not collected by default. +> [!NOTE] +> [System metrics](https://cloud.google.com/stackdriver/docs/solutions/gke/managing-metrics#enable-system-metrics) collection is pre-configured for Autopilot clusters and cannot be disabled. -> **Note** -> System metrics collection is pre-configured for Autopilot clusters and cannot be disabled. - -> **Warning** +> [!WARNING] > GKE **workload metrics** is deprecated and removed in GKE 1.24 and later. Workload metrics is replaced by [Google Cloud Managed Service for Prometheus](https://cloud.google.com/stackdriver/docs/managed-prometheus), which is Google's recommended way to monitor Kubernetes applications by using Cloud Monitoring. -```hcl -module "cluster-1" { - source = "./fabric/modules/gke-cluster-autopilot" - project_id = var.project_id - name = "cluster-1" - location = "europe-west1" - vpc_config = { - network = var.vpc.self_link - subnetwork = var.subnet.self_link - } - monitoring_config = { - enable_api_server_metrics = true - enable_controller_manager_metrics = true - enable_scheduler_metrics = true - } -} -# tftest modules=1 resources=1 inventory=monitoring-config-control-plane.yaml -``` - -### Backup for GKE - -This example shows how to [enable the Backup for GKE agent and configure a Backup Plan](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/backup-for-gke) for GKE Standard clusters. +This example shows how to [configure collection of Kubernetes control plane metrics](https://cloud.google.com/stackdriver/docs/solutions/gke/managing-metrics#enable-control-plane-metrics). These metrics are optional and are not collected by default. ```hcl module "cluster-1" { @@ -129,7 +123,71 @@ module "cluster-1" { vpc_config = { network = var.vpc.self_link subnetwork = var.subnet.self_link - secondary_range_names = { pods = "pods", services = "services" } + secondary_range_names = {} # use default names "pods" and "services" + } + monitoring_config = { + enable_api_server_metrics = true + enable_controller_manager_metrics = true + enable_scheduler_metrics = true + } +} +# tftest modules=1 resources=1 inventory=monitoring-config-control-plane.yaml +``` + +The next example shows how to [configure collection of kube state metrics](https://cloud.google.com/stackdriver/docs/solutions/gke/managing-metrics#enable-ksm). These metrics are optional and are not collected by default. + +```hcl +module "cluster-1" { + source = "./fabric/modules/gke-cluster-autopilot" + project_id = var.project_id + name = "cluster-1" + location = "europe-west1" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} # use default names "pods" and "services" + } + monitoring_config = { + enable_daemonset_metrics = true + enable_deployment_metrics = true + enable_hpa_metrics = true + enable_pod_metrics = true + enable_statefulset_metrics = true + enable_storage_metrics = true + # Kube state metrics collection requires Google Cloud Managed Service for Prometheus, + # which is enabled by default. + # enable_managed_prometheus = true + } +} +# tftest modules=1 resources=1 inventory=monitoring-config-kube-state.yaml +``` + +The *control plane metrics* and *kube state metrics* collection can be configured in a single `monitoring_config` block. + +### Backup for GKE + +> [!NOTE] +> Although Backup for GKE can be enabled as an add-on when configuring your GKE clusters, it is a separate service from GKE. + +[Backup for GKE](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/backup-for-gke) is a service for backing up and restoring workloads in GKE clusters. It has two components: + +* A [Google Cloud API](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/reference/rest) that serves as the control plane for the service. +* A GKE add-on (the [Backup for GKE agent](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/backup-for-gke#agent_overview)) that must be enabled in each cluster for which you wish to perform backup and restore operations. + +Backup for GKE is supported in GKE Autopilot clusters with [some restrictions](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/about-autopilot). + +This example shows how to [enable Backup for GKE on a new Autopilot cluster](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/how-to/install#enable_on_a_new_cluster_optional) and [plan a set of backups](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/how-to/backup-plan). + +```hcl +module "cluster-1" { + source = "./fabric/modules/gke-cluster-autopilot" + project_id = var.project_id + name = "cluster-1" + location = "europe-west1" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} } backup_configs = { enable_backup_agent = true @@ -148,10 +206,10 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [location](variables.tf#L110) | Autopilot cluster are always regional. | string | ✓ | | -| [name](variables.tf#L170) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L196) | Cluster project id. | string | ✓ | | -| [vpc_config](variables.tf#L224) | VPC-level configuration. | object({…}) | ✓ | | +| [location](variables.tf#L110) | Autopilot clusters are always regional. | string | ✓ | | +| [name](variables.tf#L187) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L213) | Cluster project ID. | string | ✓ | | +| [vpc_config](variables.tf#L242) | VPC-level configuration. | object({…}) | ✓ | | | [backup_configs](variables.tf#L17) | Configuration for Backup for GKE. | object({…}) | | {} | | [description](variables.tf#L37) | Cluster description. | string | | null | | [enable_addons](variables.tf#L43) | Addons enabled in the cluster (true means enabled). | object({…}) | | {…} | @@ -161,12 +219,12 @@ module "cluster-1" { | [logging_config](variables.tf#L115) | Logging configuration. | object({…}) | | {} | | [maintenance_config](variables.tf#L126) | Maintenance window configuration. | object({…}) | | {…} | | [min_master_version](variables.tf#L149) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L155) | Monitoring configuration. System metrics collection cannot be disabled for Autopilot clusters. Control plane metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | -| [node_locations](variables.tf#L175) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [private_cluster_config](variables.tf#L182) | Private cluster configuration. | object({…}) | | null | -| [release_channel](variables.tf#L201) | Release channel for GKE upgrades. Clusters created in the Autopilot mode must use a release channel. Choose between \"RAPID\", \"REGULAR\", and \"STABLE\". | string | | "REGULAR" | -| [service_account](variables.tf#L212) | The Google Cloud Platform Service Account to be used by the node VMs created by GKE Autopilot. | string | | null | -| [tags](variables.tf#L218) | Network tags applied to nodes. | list(string) | | null | +| [monitoring_config](variables.tf#L155) | Monitoring configuration. System metrics collection cannot be disabled. Control plane metrics are optional. Kube state metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | +| [node_locations](variables.tf#L192) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [private_cluster_config](variables.tf#L199) | Private cluster configuration. | object({…}) | | null | +| [release_channel](variables.tf#L218) | Release channel for GKE upgrades. Clusters created in the Autopilot mode must use a release channel. Choose between \"RAPID\", \"REGULAR\", and \"STABLE\". | string | | "REGULAR" | +| [service_account](variables.tf#L229) | The Google Cloud Platform Service Account to be used by the node VMs created by GKE Autopilot. | string | | null | +| [tags](variables.tf#L235) | Network tags applied to nodes. | list(string) | | [] | ## Outputs @@ -175,7 +233,7 @@ module "cluster-1" { | [ca_certificate](outputs.tf#L17) | Public certificate of the cluster (base64-encoded). | ✓ | | [cluster](outputs.tf#L23) | Cluster resource. | ✓ | | [endpoint](outputs.tf#L29) | Cluster endpoint. | | -| [id](outputs.tf#L34) | Fully qualified cluster id. | | +| [id](outputs.tf#L34) | Fully qualified cluster ID. | | | [location](outputs.tf#L39) | Cluster location. | | | [master_version](outputs.tf#L44) | Master version. | | | [name](outputs.tf#L49) | Cluster name. | | diff --git a/modules/gke-cluster-autopilot/main.tf b/modules/gke-cluster-autopilot/main.tf index 330c4993..4ca8ee54 100644 --- a/modules/gke-cluster-autopilot/main.tf +++ b/modules/gke-cluster-autopilot/main.tf @@ -103,12 +103,19 @@ resource "google_container_cluster" "cluster" { } } + dynamic "gateway_api_config" { + for_each = var.enable_features.gateway_api ? [""] : [] + content { + channel = "CHANNEL_STANDARD" + } + } + dynamic "ip_allocation_policy" { for_each = var.vpc_config.secondary_range_blocks != null ? [""] : [] content { cluster_ipv4_cidr_block = var.vpc_config.secondary_range_blocks.pods services_ipv4_cidr_block = var.vpc_config.secondary_range_blocks.services - stack_type = try(var.vpc_config.stack_type, null) + stack_type = var.vpc_config.stack_type } } @@ -117,7 +124,7 @@ resource "google_container_cluster" "cluster" { content { cluster_secondary_range_name = var.vpc_config.secondary_range_names.pods services_secondary_range_name = var.vpc_config.secondary_range_names.services - stack_type = try(var.vpc_config.stack_type, null) + stack_type = var.vpc_config.stack_type } } @@ -131,13 +138,6 @@ resource "google_container_cluster" "cluster" { ])) } - dynamic "gateway_api_config" { - for_each = var.enable_features.gateway_api ? [""] : [] - content { - channel = "CHANNEL_STANDARD" - } - } - maintenance_policy { dynamic "daily_maintenance_window" { for_each = ( @@ -207,10 +207,17 @@ resource "google_container_cluster" "cluster" { enable_components = toset(compact([ # System metrics collection cannot be disabled for Autopilot clusters. "SYSTEM_COMPONENTS", - # Control plane metrics. + # Control plane metrics: var.monitoring_config.enable_api_server_metrics ? "APISERVER" : null, var.monitoring_config.enable_controller_manager_metrics ? "CONTROLLER_MANAGER" : null, var.monitoring_config.enable_scheduler_metrics ? "SCHEDULER" : null, + # Kube state metrics: + var.monitoring_config.enable_daemonset_metrics ? "DAEMONSET" : null, + var.monitoring_config.enable_deployment_metrics ? "DEPLOYMENT" : null, + var.monitoring_config.enable_hpa_metrics ? "HPA" : null, + var.monitoring_config.enable_pod_metrics ? "POD" : null, + var.monitoring_config.enable_statefulset_metrics ? "STATEFULSET" : null, + var.monitoring_config.enable_storage_metrics ? "STORAGE" : null, ])) managed_prometheus { enabled = var.monitoring_config.enable_managed_prometheus @@ -231,6 +238,15 @@ resource "google_container_cluster" "cluster" { } } + dynamic "node_pool_auto_config" { + for_each = length(var.tags) > 0 ? [""] : [] + content { + network_tags { + tags = toset(var.tags) + } + } + } + dynamic "private_cluster_config" { for_each = ( var.private_cluster_config != null ? [""] : [] diff --git a/modules/gke-cluster-autopilot/outputs.tf b/modules/gke-cluster-autopilot/outputs.tf index 029ab06a..7978e55b 100644 --- a/modules/gke-cluster-autopilot/outputs.tf +++ b/modules/gke-cluster-autopilot/outputs.tf @@ -32,7 +32,7 @@ output "endpoint" { } output "id" { - description = "Fully qualified cluster id." + description = "Fully qualified cluster ID." value = google_container_cluster.cluster.id } diff --git a/modules/gke-cluster-autopilot/variables.tf b/modules/gke-cluster-autopilot/variables.tf index 52896bbd..24f8cd2b 100644 --- a/modules/gke-cluster-autopilot/variables.tf +++ b/modules/gke-cluster-autopilot/variables.tf @@ -108,7 +108,7 @@ variable "labels" { } variable "location" { - description = "Autopilot cluster are always regional." + description = "Autopilot clusters are always regional." type = string } @@ -153,18 +153,35 @@ variable "min_master_version" { } variable "monitoring_config" { - description = "Monitoring configuration. System metrics collection cannot be disabled for Autopilot clusters. Control plane metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default." + description = "Monitoring configuration. System metrics collection cannot be disabled. Control plane metrics are optional. Kube state metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default." type = object({ # Control plane metrics enable_api_server_metrics = optional(bool, false) enable_controller_manager_metrics = optional(bool, false) enable_scheduler_metrics = optional(bool, false) - # Google Cloud Managed Service for Prometheus - # GKE Autopilot clusters running GKE version 1.25 or greater must have this on. + # Kube state metrics. Requires managed Prometheus. Requires provider version >= v4.82.0 + enable_daemonset_metrics = optional(bool, false) + enable_deployment_metrics = optional(bool, false) + enable_hpa_metrics = optional(bool, false) + enable_pod_metrics = optional(bool, false) + enable_statefulset_metrics = optional(bool, false) + enable_storage_metrics = optional(bool, false) + # Google Cloud Managed Service for Prometheus. Autopilot clusters version >= 1.25 must have this on. enable_managed_prometheus = optional(bool, true) }) default = {} nullable = false + validation { + condition = anytrue([ + var.monitoring_config.enable_daemonset_metrics, + var.monitoring_config.enable_deployment_metrics, + var.monitoring_config.enable_hpa_metrics, + var.monitoring_config.enable_pod_metrics, + var.monitoring_config.enable_statefulset_metrics, + var.monitoring_config.enable_storage_metrics, + ]) ? var.monitoring_config.enable_managed_prometheus : true + error_message = "Kube state metrics collection requires Google Cloud Managed Service for Prometheus to be enabled." + } } variable "name" { @@ -194,7 +211,7 @@ variable "private_cluster_config" { } variable "project_id" { - description = "Cluster project id." + description = "Cluster project ID." type = string } @@ -218,7 +235,8 @@ variable "service_account" { variable "tags" { description = "Network tags applied to nodes." type = list(string) - default = null + default = [] + nullable = false } variable "vpc_config" { @@ -232,9 +250,9 @@ variable "vpc_config" { services = string })) secondary_range_names = optional(object({ - pods = string - services = string - }), { pods = "pods", services = "services" }) + pods = optional(string, "pods") + services = optional(string, "services") + })) master_authorized_ranges = optional(map(string)) stack_type = optional(string) }) diff --git a/modules/gke-cluster-autopilot/versions.tf b/modules/gke-cluster-autopilot/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/gke-cluster-autopilot/versions.tf +++ b/modules/gke-cluster-autopilot/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/gke-cluster-standard/README.md b/modules/gke-cluster-standard/README.md index e80a4e6d..3c9b1eb8 100644 --- a/modules/gke-cluster-standard/README.md +++ b/modules/gke-cluster-standard/README.md @@ -1,10 +1,29 @@ -# GKE cluster Standard module +# GKE Standard cluster module -This module allows simplified creation and management of GKE Standard clusters and should be used together with the GKE nodepool module, as the default nodepool is turned off here and cannot be re-enabled. Some sensible defaults are set initially, in order to allow less verbose usage for most use cases. +This module offers a way to create and manage Google Kubernetes Engine (GKE) [Standard clusters](https://cloud.google.com/kubernetes-engine/docs/concepts/choose-cluster-mode#why-standard). With its sensible default settings based on best practices and authors' experience as Google Cloud practitioners, the module accommodates for many common use cases out-of-the-box, without having to rely on verbose configuration. + +> [!IMPORTANT] +> This module should be used together with the [`gke-nodepool`](../gke-nodepool/) module because the default node pool is deleted upon cluster creation and cannot be re-created. + + +- [Example](#example) + - [GKE Standard cluster](#gke-standard-cluster) + - [Enable Dataplane V2](#enable-dataplane-v2) + - [Managing GKE logs](#managing-gke-logs) + - [Monitoring configuration](#monitoring-configuration) + - [Disable GKE logs or metrics collection](#disable-gke-logs-or-metrics-collection) + - [Cloud DNS](#cloud-dns) + - [Backup for GKE](#backup-for-gke) + - [Automatic creation of new secondary ranges](#automatic-creation-of-new-secondary-ranges) +- [Variables](#variables) +- [Outputs](#outputs) + ## Example -### GKE Cluster +### GKE Standard cluster + +This example shows how to [create a zonal GKE cluster in Standard mode](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-zonal-cluster). ```hcl module "cluster-1" { @@ -36,7 +55,9 @@ module "cluster-1" { # tftest modules=1 resources=1 inventory=basic.yaml ``` -### GKE Cluster with Dataplane V2 enabled +### Enable Dataplane V2 + +This example shows how to [create a zonal GKE Cluster with Dataplane V2 enabled](https://cloud.google.com/kubernetes-engine/docs/how-to/dataplane-v2). ```hcl module "cluster-1" { @@ -45,12 +66,9 @@ module "cluster-1" { name = "cluster-dataplane-v2" location = "europe-west1-b" vpc_config = { - network = var.vpc.self_link - subnetwork = var.subnet.self_link - secondary_range_names = { - pods = "pods" - services = "services" - } + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} # use default names "pods" and "services" master_authorized_ranges = { internal-vms = "10.0.0.0/8" } @@ -84,8 +102,9 @@ module "cluster-1" { name = "cluster-1" location = "europe-west1-b" vpc_config = { - network = var.vpc.self_link - subnetwork = var.subnet.self_link + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} } logging_config = { enable_workloads_logs = true @@ -97,14 +116,9 @@ module "cluster-1" { # tftest modules=1 resources=1 inventory=logging-config-enable-all.yaml ``` -### Disable GKE logs collection +### Monitoring configuration -This example shows how to fully disable logs collection on a GKE Standard cluster. This is not recommended. - -> **Warning** -> If you've disabled Cloud Logging or Cloud Monitoring, GKE customer support -> is offered on a best-effort basis and might require additional effort -> from your engineering team. +This example shows how to [configure collection of Kubernetes control plane metrics](https://cloud.google.com/stackdriver/docs/solutions/gke/managing-metrics#enable-control-plane-metrics). These metrics are optional and are not collected by default. ```hcl module "cluster-1" { @@ -113,8 +127,68 @@ module "cluster-1" { name = "cluster-1" location = "europe-west1-b" vpc_config = { - network = var.vpc.self_link - subnetwork = var.subnet.self_link + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} # use default names "pods" and "services" + } + monitoring_config = { + enable_api_server_metrics = true + enable_controller_manager_metrics = true + enable_scheduler_metrics = true + } +} +# tftest modules=1 resources=1 inventory=monitoring-config-control-plane.yaml +``` + +The next example shows how to [configure collection of kube state metrics](https://cloud.google.com/stackdriver/docs/solutions/gke/managing-metrics#enable-ksm). These metrics are optional and are not collected by default. + +```hcl +module "cluster-1" { + source = "./fabric/modules/gke-cluster-standard" + project_id = "myproject" + name = "cluster-1" + location = "europe-west1-b" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} # use default names "pods" and "services" + } + monitoring_config = { + enable_daemonset_metrics = true + enable_deployment_metrics = true + enable_hpa_metrics = true + enable_pod_metrics = true + enable_statefulset_metrics = true + enable_storage_metrics = true + # Kube state metrics collection requires Google Cloud Managed Service for Prometheus, + # which is enabled by default. + # enable_managed_prometheus = true + } +} +# tftest modules=1 resources=1 inventory=monitoring-config-kube-state.yaml +``` + +The *control plane metrics* and *kube state metrics* collection can be configured in a single `monitoring_config` block. + +### Disable GKE logs or metrics collection + +> [!WARNING] +> If you've disabled Cloud Logging or Cloud Monitoring, GKE customer support +> is offered on a best-effort basis and might require additional effort +> from your engineering team. + +This example shows how to fully disable logs collection on a zonal GKE Standard cluster. This is not recommended. + +```hcl +module "cluster-1" { + source = "./fabric/modules/gke-cluster-standard" + project_id = "myproject" + name = "cluster-1" + location = "europe-west1-b" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} } logging_config = { enable_system_logs = false @@ -123,6 +197,27 @@ module "cluster-1" { # tftest modules=1 resources=1 inventory=logging-config-disable-all.yaml ``` +The next example shows how to fully disable metrics collection on a zonal GKE Standard cluster. This is not recommended. + +```hcl +module "cluster-1" { + source = "./fabric/modules/gke-cluster-standard" + project_id = "myproject" + name = "cluster-1" + location = "europe-west1-b" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = {} + } + monitoring_config = { + enable_system_metrics = false + enable_managed_prometheus = false + } +} +# tftest modules=1 resources=1 inventory=monitoring-config-disable-all.yaml +``` + ### Cloud DNS This example shows how to [use Cloud DNS as a Kubernetes DNS provider](https://cloud.google.com/kubernetes-engine/docs/how-to/cloud-dns) for GKE Standard clusters. @@ -136,7 +231,7 @@ module "cluster-1" { vpc_config = { network = var.vpc.self_link subnetwork = var.subnet.self_link - secondary_range_names = { pods = "pods", services = "services" } + secondary_range_names = {} } enable_features = { dns = { @@ -151,7 +246,15 @@ module "cluster-1" { ### Backup for GKE -This example shows how to [enable the Backup for GKE agent and configure a Backup Plan](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/backup-for-gke) for GKE Standard clusters. +> [!NOTE] +> Although Backup for GKE can be enabled as an add-on when configuring your GKE clusters, it is a separate service from GKE. + +[Backup for GKE](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/backup-for-gke) is a service for backing up and restoring workloads in GKE clusters. It has two components: + +* A [Google Cloud API](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/reference/rest) that serves as the control plane for the service. +* A GKE add-on (the [Backup for GKE agent](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/backup-for-gke#agent_overview)) that must be enabled in each cluster for which you wish to perform backup and restore operations. + +This example shows how to [enable Backup for GKE on a new zonal GKE Standard cluster](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/how-to/install#enable_on_a_new_cluster_optional) and [plan a set of backups](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/how-to/backup-plan). ```hcl module "cluster-1" { @@ -162,7 +265,7 @@ module "cluster-1" { vpc_config = { network = var.vpc.self_link subnetwork = var.subnet.self_link - secondary_range_names = { pods = "pods", services = "services" } + secondary_range_names = {} } backup_configs = { enable_backup_agent = true @@ -176,15 +279,37 @@ module "cluster-1" { } # tftest modules=1 resources=2 inventory=backup.yaml ``` + +### Automatic creation of new secondary ranges + +You can use `var.vpc_config.secondary_range_blocks` to let GKE create new secondary ranges for the cluster. The example below reserves an available /14 block for pods and a /20 for services. + +```hcl +module "cluster-1" { + source = "./fabric/modules/gke-cluster-standard" + project_id = var.project_id + name = "cluster-1" + location = "europe-west1-b" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_blocks = { + pods = "" + services = "/20" # can be an empty string as well + } + } +} +# tftest modules=1 resources=1 +``` ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [location](variables.tf#L138) | Cluster zone or region. | string | ✓ | | -| [name](variables.tf#L210) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L236) | Cluster project id. | string | ✓ | | -| [vpc_config](variables.tf#L253) | VPC-level configuration. | object({…}) | ✓ | | +| [name](variables.tf#L249) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L275) | Cluster project id. | string | ✓ | | +| [vpc_config](variables.tf#L292) | VPC-level configuration. | object({…}) | ✓ | | | [backup_configs](variables.tf#L17) | Configuration for Backup for GKE. | object({…}) | | {} | | [cluster_autoscaling](variables.tf#L37) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | | [description](variables.tf#L58) | Cluster description. | string | | null | @@ -196,11 +321,11 @@ module "cluster-1" { | [maintenance_config](variables.tf#L164) | Maintenance window configuration. | object({…}) | | {…} | | [max_pods_per_node](variables.tf#L187) | Maximum number of pods per node in this cluster. | number | | 110 | | [min_master_version](variables.tf#L193) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L199) | Monitoring components. | object({…}) | | {…} | -| [node_locations](variables.tf#L215) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [private_cluster_config](variables.tf#L222) | Private cluster configuration. | object({…}) | | null | -| [release_channel](variables.tf#L241) | Release channel for GKE upgrades. | string | | null | -| [tags](variables.tf#L247) | Network tags applied to nodes. | list(string) | | null | +| [monitoring_config](variables.tf#L199) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | +| [node_locations](variables.tf#L254) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [private_cluster_config](variables.tf#L261) | Private cluster configuration. | object({…}) | | null | +| [release_channel](variables.tf#L280) | Release channel for GKE upgrades. | string | | null | +| [tags](variables.tf#L286) | Network tags applied to nodes. | list(string) | | null | ## Outputs diff --git a/modules/gke-cluster-standard/main.tf b/modules/gke-cluster-standard/main.tf index 8f0df84f..d27f6ab3 100644 --- a/modules/gke-cluster-standard/main.tf +++ b/modules/gke-cluster-standard/main.tf @@ -40,8 +40,8 @@ resource "google_container_cluster" "cluster" { : "DATAPATH_PROVIDER_UNSPECIFIED" ) - # the default nodepool is deleted here, use the gke-nodepool module instead - # default nodepool configuration based on a shielded_nodes variable + # the default node pool is deleted here, use the gke-nodepool module instead. + # the default node pool configuration is based on a shielded_nodes variable. node_config { dynamic "shielded_instance_config" { for_each = var.enable_features.shielded_nodes ? [""] : [] @@ -164,12 +164,19 @@ resource "google_container_cluster" "cluster" { } } + dynamic "gateway_api_config" { + for_each = var.enable_features.gateway_api ? [""] : [] + content { + channel = "CHANNEL_STANDARD" + } + } + dynamic "ip_allocation_policy" { for_each = var.vpc_config.secondary_range_blocks != null ? [""] : [] content { cluster_ipv4_cidr_block = var.vpc_config.secondary_range_blocks.pods services_ipv4_cidr_block = var.vpc_config.secondary_range_blocks.services - stack_type = try(var.vpc_config.stack_type, null) + stack_type = var.vpc_config.stack_type } } dynamic "ip_allocation_policy" { @@ -177,7 +184,7 @@ resource "google_container_cluster" "cluster" { content { cluster_secondary_range_name = var.vpc_config.secondary_range_names.pods services_secondary_range_name = var.vpc_config.secondary_range_names.services - stack_type = try(var.vpc_config.stack_type, null) + stack_type = var.vpc_config.stack_type } } @@ -205,13 +212,6 @@ resource "google_container_cluster" "cluster" { } } - dynamic "gateway_api_config" { - for_each = var.enable_features.gateway_api ? [""] : [] - content { - channel = "CHANNEL_STANDARD" - } - } - maintenance_policy { dynamic "daily_maintenance_window" { for_each = ( @@ -277,22 +277,28 @@ resource "google_container_cluster" "cluster" { } } - dynamic "monitoring_config" { - for_each = var.monitoring_config != null ? [""] : [] - content { - enable_components = var.monitoring_config.enable_components - dynamic "managed_prometheus" { - for_each = ( - try(var.monitoring_config.managed_prometheus, null) == true ? [""] : [] - ) - content { - enabled = true - } - } + monitoring_config { + enable_components = toset(compact([ + # System metrics is the minimum requirement if any other metrics are enabled. This is checked by input var validation. + var.monitoring_config.enable_system_metrics ? "SYSTEM_COMPONENTS" : null, + # Control plane metrics + var.monitoring_config.enable_api_server_metrics ? "APISERVER" : null, + var.monitoring_config.enable_controller_manager_metrics ? "CONTROLLER_MANAGER" : null, + var.monitoring_config.enable_scheduler_metrics ? "SCHEDULER" : null, + # Kube state metrics + var.monitoring_config.enable_daemonset_metrics ? "DAEMONSET" : null, + var.monitoring_config.enable_deployment_metrics ? "DEPLOYMENT" : null, + var.monitoring_config.enable_hpa_metrics ? "HPA" : null, + var.monitoring_config.enable_pod_metrics ? "POD" : null, + var.monitoring_config.enable_statefulset_metrics ? "STATEFULSET" : null, + var.monitoring_config.enable_storage_metrics ? "STORAGE" : null, + ])) + managed_prometheus { + enabled = var.monitoring_config.enable_managed_prometheus } } - # dataplane v2 has built-in network policies + # Dataplane V2 has built-in network policies dynamic "network_policy" { for_each = ( var.enable_addons.network_policy && !var.enable_features.dataplane_v2 diff --git a/modules/gke-cluster-standard/variables.tf b/modules/gke-cluster-standard/variables.tf index b9c4a113..6b76efa7 100644 --- a/modules/gke-cluster-standard/variables.tf +++ b/modules/gke-cluster-standard/variables.tf @@ -197,13 +197,52 @@ variable "min_master_version" { } variable "monitoring_config" { - description = "Monitoring components." + description = "Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default." type = object({ - enable_components = optional(list(string)) - managed_prometheus = optional(bool) + enable_system_metrics = optional(bool, true) + + # Control plane metrics + enable_api_server_metrics = optional(bool, false) + enable_controller_manager_metrics = optional(bool, false) + enable_scheduler_metrics = optional(bool, false) + + # Kube state metrics + enable_daemonset_metrics = optional(bool, false) + enable_deployment_metrics = optional(bool, false) + enable_hpa_metrics = optional(bool, false) + enable_pod_metrics = optional(bool, false) + enable_statefulset_metrics = optional(bool, false) + enable_storage_metrics = optional(bool, false) + + # Google Cloud Managed Service for Prometheus + enable_managed_prometheus = optional(bool, true) }) - default = { - enable_components = ["SYSTEM_COMPONENTS"] + default = {} + nullable = false + validation { + condition = anytrue([ + var.monitoring_config.enable_api_server_metrics, + var.monitoring_config.enable_controller_manager_metrics, + var.monitoring_config.enable_scheduler_metrics, + var.monitoring_config.enable_daemonset_metrics, + var.monitoring_config.enable_deployment_metrics, + var.monitoring_config.enable_hpa_metrics, + var.monitoring_config.enable_pod_metrics, + var.monitoring_config.enable_statefulset_metrics, + var.monitoring_config.enable_storage_metrics, + ]) ? var.monitoring_config.enable_system_metrics : true + error_message = "System metrics are the minimum required component for enabling metrics collection." + } + validation { + condition = anytrue([ + var.monitoring_config.enable_daemonset_metrics, + var.monitoring_config.enable_deployment_metrics, + var.monitoring_config.enable_hpa_metrics, + var.monitoring_config.enable_pod_metrics, + var.monitoring_config.enable_statefulset_metrics, + var.monitoring_config.enable_storage_metrics, + ]) ? var.monitoring_config.enable_managed_prometheus : true + error_message = "Kube state metrics collection requires Google Cloud Managed Service for Prometheus to be enabled." } } @@ -261,9 +300,9 @@ variable "vpc_config" { services = string })) secondary_range_names = optional(object({ - pods = string - services = string - }), { pods = "pods", services = "services" }) + pods = optional(string, "pods") + services = optional(string, "services") + })) master_authorized_ranges = optional(map(string)) stack_type = optional(string) }) diff --git a/modules/gke-cluster-standard/versions.tf b/modules/gke-cluster-standard/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/gke-cluster-standard/versions.tf +++ b/modules/gke-cluster-standard/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/gke-hub/versions.tf b/modules/gke-hub/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/gke-hub/versions.tf +++ b/modules/gke-hub/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/gke-nodepool/versions.tf b/modules/gke-nodepool/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/gke-nodepool/versions.tf +++ b/modules/gke-nodepool/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/iam-service-account/README.md b/modules/iam-service-account/README.md index 9fd6cba0..ea3362c7 100644 --- a/modules/iam-service-account/README.md +++ b/modules/iam-service-account/README.md @@ -45,23 +45,23 @@ module "myproject-default-service-accounts" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L113) | Name of the service account to create. | string | ✓ | | -| [project_id](variables.tf#L128) | Project id where service account will be created. | string | ✓ | | +| [name](variables.tf#L114) | Name of the service account to create. | string | ✓ | | +| [project_id](variables.tf#L129) | Project id where service account will be created. | string | ✓ | | | [description](variables.tf#L17) | Optional description. | string | | null | | [display_name](variables.tf#L23) | Display name of the service account to create. | string | | "Terraform-managed." | | [generate_key](variables.tf#L29) | Generate a key for service account. | bool | | false | | [iam](variables.tf#L35) | IAM bindings on the service account in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_billing_roles](variables.tf#L42) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L49) | Authoritative IAM bindings on the service account in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L63) | Individual additive IAM bindings on the service account. Keys are arbitrary. | map(object({…})) | | {} | -| [iam_folder_roles](variables.tf#L78) | Folder roles granted to this service account, by folder id. Non-authoritative. | map(list(string)) | | {} | -| [iam_organization_roles](variables.tf#L85) | Organization roles granted to this service account, by organization id. Non-authoritative. | map(list(string)) | | {} | -| [iam_project_roles](variables.tf#L92) | Project roles granted to this service account, by project id. | map(list(string)) | | {} | -| [iam_sa_roles](variables.tf#L99) | Service account roles granted to this service account, by service account name. | map(list(string)) | | {} | -| [iam_storage_roles](variables.tf#L106) | Storage roles granted to this service account, by bucket name. | map(list(string)) | | {} | -| [prefix](variables.tf#L118) | Prefix applied to service account names. | string | | null | -| [public_keys_directory](variables.tf#L133) | Path to public keys data files to upload to the service account (should have `.pem` extension). | string | | "" | -| [service_account_create](variables.tf#L139) | Create service account. When set to false, uses a data source to reference an existing service account. | bool | | true | +| [iam_bindings](variables.tf#L49) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L64) | Individual additive IAM bindings on the service account. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_folder_roles](variables.tf#L79) | Folder roles granted to this service account, by folder id. Non-authoritative. | map(list(string)) | | {} | +| [iam_organization_roles](variables.tf#L86) | Organization roles granted to this service account, by organization id. Non-authoritative. | map(list(string)) | | {} | +| [iam_project_roles](variables.tf#L93) | Project roles granted to this service account, by project id. | map(list(string)) | | {} | +| [iam_sa_roles](variables.tf#L100) | Service account roles granted to this service account, by service account name. | map(list(string)) | | {} | +| [iam_storage_roles](variables.tf#L107) | Storage roles granted to this service account, by bucket name. | map(list(string)) | | {} | +| [prefix](variables.tf#L119) | Prefix applied to service account names. | string | | null | +| [public_keys_directory](variables.tf#L134) | Path to public keys data files to upload to the service account (should have `.pem` extension). | string | | "" | +| [service_account_create](variables.tf#L140) | Create service account. When set to false, uses a data source to reference an existing service account. | bool | | true | ## Outputs diff --git a/modules/iam-service-account/iam.tf b/modules/iam-service-account/iam.tf index a9423fb0..15ae1acc 100644 --- a/modules/iam-service-account/iam.tf +++ b/modules/iam-service-account/iam.tf @@ -71,7 +71,7 @@ resource "google_service_account_iam_binding" "authoritative" { resource "google_service_account_iam_binding" "bindings" { for_each = var.iam_bindings service_account_id = local.service_account.name - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/iam-service-account/variables.tf b/modules/iam-service-account/variables.tf index c9ca7069..4a75af46 100644 --- a/modules/iam-service-account/variables.tf +++ b/modules/iam-service-account/variables.tf @@ -47,9 +47,10 @@ variable "iam_billing_roles" { } variable "iam_bindings" { - description = "Authoritative IAM bindings on the service account in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/iam-service-account/versions.tf b/modules/iam-service-account/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/iam-service-account/versions.tf +++ b/modules/iam-service-account/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/kms/README.md b/modules/kms/README.md index 56acff46..ddbf4b5c 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -31,7 +31,7 @@ module "kms" { } keyring = { location = "europe-west1", name = "test" } keyring_create = false - keys = { key-a = null, key-b = null, key-c = null } + keys = { key-a = {}, key-b = {}, key-c = {} } } # tftest skip (uses data sources) ``` @@ -42,26 +42,34 @@ module "kms" { module "kms" { source = "./fabric/modules/kms" project_id = "my-project" - key_iam = { - key-a = { - "roles/cloudkms.admin" = ["user:user3@example.com"] - } + keyring = { + location = "europe-west1" + name = "test" } - key_iam_bindings_additive = { - key-b-am1 = { - key = "key-b" - member = "user:am1@example.com" - role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - } - } - keyring = { location = "europe-west1", name = "test" } keys = { - key-a = null - key-b = { rotation_period = "604800s", labels = null } - key-c = { rotation_period = null, labels = { env = "test" } } + key-a = { + iam = { + "roles/cloudkms.admin" = ["user:user3@example.com"] + } + } + key-b = { + rotation_period = "604800s" + iam_bindings_additive = { + key-b-iam1 = { + key = "key-b" + member = "user:am1@example.com" + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + } + } + } + key-c = { + labels = { + env = "test" + } + } } } -# tftest modules=1 resources=6 +# tftest modules=1 resources=6 inventory=basic.yaml ``` ### Crypto key purpose @@ -70,38 +78,35 @@ module "kms" { module "kms" { source = "./fabric/modules/kms" project_id = "my-project" - key_purpose = { - key-c = { + keyring = { + location = "europe-west1" + name = "test" + } + keys = { + key-a = { purpose = "ASYMMETRIC_SIGN" version_template = { algorithm = "EC_SIGN_P384_SHA384" - protection_level = null + protection_level = "HSM" } } } - keyring = { location = "europe-west1", name = "test" } - keys = { key-a = null, key-b = null, key-c = null } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=2 inventory=purpose.yaml ``` ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [keyring](variables.tf#L117) | Keyring attributes. | object({…}) | ✓ | | -| [project_id](variables.tf#L140) | Project id where the keyring will be created. | string | ✓ | | +| [keyring](variables.tf#L54) | Keyring attributes. | object({…}) | ✓ | | +| [project_id](variables.tf#L103) | Project id where the keyring will be created. | string | ✓ | | | [iam](variables.tf#L17) | Keyring IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L23) | Keyring authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L37) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [key_iam](variables.tf#L52) | Key IAM bindings in {KEY => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {} | -| [key_iam_bindings](variables.tf#L58) | Key authoritative IAM bindings in {KEY => {ROLE => {members = [], condition = {}}}}. | map(object({…})) | | {} | -| [key_iam_bindings_additive](variables.tf#L72) | Key individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [key_purpose](variables.tf#L88) | Per-key purpose, if not set defaults will be used. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required. | map(object({…})) | | {} | -| [key_purpose_defaults](variables.tf#L100) | Defaults used for key purpose when not defined at the key level. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required. | object({…}) | | {…} | -| [keyring_create](variables.tf#L125) | Set to false to manage keys and IAM bindings in an existing keyring. | bool | | true | -| [keys](variables.tf#L131) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | -| [tag_bindings](variables.tf#L145) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | null | +| [iam_bindings](variables.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L39) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [keyring_create](variables.tf#L62) | Set to false to manage keys and IAM bindings in an existing keyring. | bool | | true | +| [keys](variables.tf#L68) | Key names and base attributes. Set attributes to null if not needed. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L108) | Tag bindings for this keyring, in key => tag value id format. | map(string) | | {} | ## Outputs diff --git a/modules/kms/iam.tf b/modules/kms/iam.tf index 9a78c2cf..8ac17a56 100644 --- a/modules/kms/iam.tf +++ b/modules/kms/iam.tf @@ -16,24 +16,36 @@ locals { key_iam = flatten([ - for key, roles in var.key_iam : [ - for role, members in roles : { - key = key + for k, v in var.keys : [ + for role, members in v.iam : { + key = k role = role members = members } ] ]) - key_iam_bindings = flatten([ - for key, roles in var.key_iam_bindings : [ - for role, data in roles : { - key = key - role = role + key_iam_bindings = merge([ + for k, v in var.keys : { + for binding_key, data in v.iam_bindings : + binding_key => { + key = k + role = data.role members = data.members condition = data.condition } - ] - ]) + } + ]...) + key_iam_bindings_additive = merge([ + for k, v in var.keys : { + for binding_key, data in v.iam_bindings_additive : + binding_key => { + key = k + role = data.role + member = data.member + condition = data.condition + } + } + ]...) } resource "google_kms_key_ring_iam_binding" "authoritative" { @@ -46,7 +58,7 @@ resource "google_kms_key_ring_iam_binding" "authoritative" { resource "google_kms_key_ring_iam_binding" "bindings" { for_each = var.iam_bindings key_ring_id = local.keyring.id - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] @@ -84,10 +96,7 @@ resource "google_kms_crypto_key_iam_binding" "authoritative" { } resource "google_kms_crypto_key_iam_binding" "bindings" { - for_each = { - for binding in local.key_iam_bindings : - "${binding.key}.${binding.role}" => binding - } + for_each = local.key_iam_bindings role = each.value.role crypto_key_id = google_kms_crypto_key.default[each.value.key].id members = each.value.members @@ -102,7 +111,7 @@ resource "google_kms_crypto_key_iam_binding" "bindings" { } resource "google_kms_crypto_key_iam_member" "members" { - for_each = var.key_iam_bindings_additive + for_each = local.key_iam_bindings_additive crypto_key_id = google_kms_crypto_key.default[each.value.key].id role = each.value.role member = each.value.member diff --git a/modules/kms/main.tf b/modules/kms/main.tf index 26624f15..6be7c812 100644 --- a/modules/kms/main.tf +++ b/modules/kms/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. @@ -15,11 +15,6 @@ */ locals { - key_purpose = { - for key, attrs in var.keys : key => try( - var.key_purpose[key], var.key_purpose_defaults - ) - } keyring = ( var.keyring_create ? google_kms_key_ring.default.0 @@ -42,17 +37,19 @@ resource "google_kms_key_ring" "default" { } resource "google_kms_crypto_key" "default" { - for_each = var.keys - key_ring = local.keyring.id - name = each.key - rotation_period = try(each.value.rotation_period, null) - labels = try(each.value.labels, null) - purpose = try(local.key_purpose[each.key].purpose, null) + for_each = var.keys + key_ring = local.keyring.id + name = each.key + rotation_period = each.value.rotation_period + labels = each.value.labels + purpose = each.value.purpose + skip_initial_version_creation = each.value.skip_initial_version_creation + dynamic "version_template" { - for_each = local.key_purpose[each.key].version_template == null ? [] : [""] + for_each = each.value.version_template == null ? [] : [""] content { - algorithm = local.key_purpose[each.key].version_template.algorithm - protection_level = local.key_purpose[each.key].version_template.protection_level + algorithm = each.value.version_template.algorithm + protection_level = each.value.version_template.protection_level } } } diff --git a/modules/kms/tags.tf b/modules/kms/tags.tf index 894c28aa..c0955c62 100644 --- a/modules/kms/tags.tf +++ b/modules/kms/tags.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. @@ -15,7 +15,7 @@ */ resource "google_tags_tag_binding" "binding" { - for_each = coalesce(var.tag_bindings, {}) + for_each = var.tag_bindings parent = "//cloudresourcemanager.googleapis.com/${local.keyring.id}" tag_value = each.value } diff --git a/modules/kms/variables.tf b/modules/kms/variables.tf index 44c98036..30861764 100644 --- a/modules/kms/variables.tf +++ b/modules/kms/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. @@ -18,12 +18,14 @@ variable "iam" { description = "Keyring IAM bindings in {ROLE => [MEMBERS]} format." type = map(list(string)) default = {} + nullable = false } variable "iam_bindings" { - description = "Keyring authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string @@ -49,71 +51,6 @@ variable "iam_bindings_additive" { default = {} } -variable "key_iam" { - description = "Key IAM bindings in {KEY => {ROLE => [MEMBERS]}} format." - type = map(map(list(string))) - default = {} -} - -variable "key_iam_bindings" { - description = "Key authoritative IAM bindings in {KEY => {ROLE => {members = [], condition = {}}}}." - type = map(object({ - members = list(string) - condition = optional(object({ - expression = string - title = string - description = optional(string) - })) - })) - nullable = false - default = {} -} - -variable "key_iam_bindings_additive" { - description = "Key individual additive IAM bindings. Keys are arbitrary." - type = map(object({ - key = string - member = string - role = string - condition = optional(object({ - expression = string - title = string - description = optional(string) - })) - })) - nullable = false - default = {} -} - -variable "key_purpose" { - description = "Per-key purpose, if not set defaults will be used. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required." - type = map(object({ - purpose = string - version_template = object({ - algorithm = string - protection_level = string - }) - })) - default = {} -} - -variable "key_purpose_defaults" { - description = "Defaults used for key purpose when not defined at the key level. If purpose is not `ENCRYPT_DECRYPT` (the default), `version_template.algorithm` is required." - type = object({ - purpose = string - version_template = object({ - algorithm = string - protection_level = string - }) - }) - default = { - purpose = null - version_template = null - } -} - -# cf https://cloud.google.com/kms/docs/locations - variable "keyring" { description = "Keyring attributes." type = object({ @@ -131,10 +68,36 @@ variable "keyring_create" { variable "keys" { description = "Key names and base attributes. Set attributes to null if not needed." type = map(object({ - rotation_period = string - labels = map(string) + rotation_period = optional(string) + labels = optional(map(string)) + purpose = optional(string, "ENCRYPT_DECRYPT") + skip_initial_version_creation = optional(bool, false) + version_template = optional(object({ + algorithm = string + protection_level = optional(string, "SOFTWARE") + })) + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })) - default = {} + default = {} + nullable = false } variable "project_id" { @@ -145,5 +108,6 @@ variable "project_id" { variable "tag_bindings" { description = "Tag bindings for this keyring, in key => tag value id format." type = map(string) - default = null + default = {} + nullable = false } diff --git a/modules/kms/versions.tf b/modules/kms/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/kms/versions.tf +++ b/modules/kms/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/logging-bucket/versions.tf b/modules/logging-bucket/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/logging-bucket/versions.tf +++ b/modules/logging-bucket/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/ncc-spoke-ra/versions.tf b/modules/ncc-spoke-ra/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/ncc-spoke-ra/versions.tf +++ b/modules/ncc-spoke-ra/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-address/versions.tf b/modules/net-address/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-address/versions.tf +++ b/modules/net-address/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-cloudnat/versions.tf b/modules/net-cloudnat/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-cloudnat/versions.tf +++ b/modules/net-cloudnat/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-firewall-policy/versions.tf b/modules/net-firewall-policy/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-firewall-policy/versions.tf +++ b/modules/net-firewall-policy/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-ipsec-over-interconnect/versions.tf b/modules/net-ipsec-over-interconnect/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-ipsec-over-interconnect/versions.tf +++ b/modules/net-ipsec-over-interconnect/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-lb-app-ext/versions.tf b/modules/net-lb-app-ext/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-lb-app-ext/versions.tf +++ b/modules/net-lb-app-ext/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-lb-app-int/versions.tf b/modules/net-lb-app-int/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-lb-app-int/versions.tf +++ b/modules/net-lb-app-int/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-lb-ext/versions.tf b/modules/net-lb-ext/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-lb-ext/versions.tf +++ b/modules/net-lb-ext/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-lb-int/versions.tf b/modules/net-lb-int/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-lb-int/versions.tf +++ b/modules/net-lb-int/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-lb-proxy-int/versions.tf b/modules/net-lb-proxy-int/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-lb-proxy-int/versions.tf +++ b/modules/net-lb-proxy-int/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-swp/versions.tf b/modules/net-swp/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-swp/versions.tf +++ b/modules/net-swp/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-vlan-attachment/README.md b/modules/net-vlan-attachment/README.md index b013fe08..a1711709 100644 --- a/modules/net-vlan-attachment/README.md +++ b/modules/net-vlan-attachment/README.md @@ -81,7 +81,7 @@ module "example-va" { name = google_compute_router.interconnect-router.name } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=2 ``` ### Dedicated Interconnect - Two VLAN Attachments on a single region (99.9% SLA) @@ -201,7 +201,7 @@ module "example-va-b" { edge_availability_domain = "AVAILABILITY_DOMAIN_2" } } -# tftest modules=2 resources=5 +# tftest modules=2 resources=3 ``` ### Dedicated Interconnect - Four VLAN Attachments on two regions (99.99% SLA) @@ -431,10 +431,10 @@ module "example-va-b-ew12" { edge_availability_domain = "AVAILABILITY_DOMAIN_2" } } -# tftest modules=4 resources=10 +# tftest modules=4 resources=6 ``` -### IPSec over Interconnect enabled setup +### IPSec for Dedicated Interconnect Refer to the [HA VPN over Interconnect Blueprint](../../blueprints/networking/ha-vpn-over-interconnect/) for an all-encompassing example. @@ -494,6 +494,47 @@ module "example-va-b" { } # tftest modules=2 resources=9 ``` + +### IPSec for Partner Interconnect + +```hcl +module "example-va-a" { + source = "./fabric/modules/net-vlan-attachment" + project_id = "myproject" + network = "mynet" + region = "europe-west8" + name = "encrypted-vlan-attachment-a" + description = "example-va-a vlan attachment" + peer_asn = "65001" + router_config = { + create = true + } + partner_interconnect_config = { + edge_availability_domain = "AVAILABILITY_DOMAIN_1" + } + vpn_gateways_ip_range = "10.255.255.0/29" # Allows for up to 8 tunnels +} + +module "example-va-b" { + source = "./fabric/modules/net-vlan-attachment" + project_id = "myproject" + network = "mynet" + region = "europe-west8" + name = "encrypted-vlan-attachment-b" + description = "example-va-b vlan attachment" + peer_asn = "65001" + router_config = { + create = true + } + partner_interconnect_config = { + edge_availability_domain = "AVAILABILITY_DOMAIN_2" + } + vpn_gateways_ip_range = "10.255.255.8/29" # Allows for up to 8 tunnels +} +# tftest modules=2 resources=6 +``` + + ## Variables diff --git a/modules/net-vlan-attachment/main.tf b/modules/net-vlan-attachment/main.tf index 877ec4a7..5cf5c328 100644 --- a/modules/net-vlan-attachment/main.tf +++ b/modules/net-vlan-attachment/main.tf @@ -61,7 +61,15 @@ resource "google_compute_router" "encrypted" { region = var.region encrypted_interconnect_router = true bgp { - asn = var.router_config.asn + asn = var.router_config.asn + advertise_mode = var.dedicated_interconnect_config == null ? "DEFAULT" : "CUSTOM" + dynamic "advertised_ip_ranges" { + for_each = var.dedicated_interconnect_config == null ? var.ipsec_gateway_ip_ranges : {} + content { + description = advertised_ip_ranges.key + range = advertised_ip_ranges.value + } + } } } @@ -106,13 +114,14 @@ resource "google_compute_router_interface" "default" { } resource "google_compute_router_peer" "default" { + count = var.dedicated_interconnect_config != null ? 1 : 0 name = "${var.name}-peer" project = var.project_id router = local.router region = var.region peer_ip_address = split("/", google_compute_interconnect_attachment.default.customer_router_ip_address)[0] peer_asn = var.peer_asn - interface = "${var.name}-intf" + interface = google_compute_router_interface.default[0].name advertised_route_priority = 100 advertise_mode = "CUSTOM" diff --git a/modules/net-vlan-attachment/versions.tf b/modules/net-vlan-attachment/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-vlan-attachment/versions.tf +++ b/modules/net-vlan-attachment/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-vpc-firewall/versions.tf b/modules/net-vpc-firewall/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-vpc-firewall/versions.tf +++ b/modules/net-vpc-firewall/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-vpc-peering/versions.tf b/modules/net-vpc-peering/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-vpc-peering/versions.tf +++ b/modules/net-vpc-peering/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 3aaaa2a7..ea86930e 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -112,38 +112,35 @@ module "vpc" { name = "subnet-1" region = "europe-west1" ip_cidr_range = "10.0.1.0/24" + iam = { + "roles/compute.networkUser" = [ + "user:user1@example.com", "group:group1@example.com" + ] + } + iam_bindings = { + subnet-1-iam = { + members = ["group:group2@example.com"] + role = "roles/compute.networkUser" + condition = { + expression = "resource.matchTag('123456789012/env', 'prod')" + title = "test_condition" + } + } + } }, { 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" - ] - } - } - subnet_iam_bindings = { - "europe-west1/subnet-1" = { - "roles/compute.networkUser" = { - members = ["group:group2@example.com"] - condition = { - expression = "resource.matchTag('123456789012/env', 'prod')" - title = "test_condition" + iam_bindings_additive = { + subnet-2-iam = { + member = "user:am1@example.com" + role = "roles/compute.networkUser" + subnet = "europe-west1/subnet-2" } } } - } - subnet_iam_bindings_additive = { - subnet-2-am1 = { - member = "user:am1@example.com" - role = "roles/compute.networkUser" - subnet = "europe-west1/subnet-2" - } - } + ] } # tftest modules=1 resources=8 inventory=subnet-iam.yaml ``` @@ -212,6 +209,15 @@ module "vpc-host" { pods = "172.16.0.0/20" services = "192.168.0.0/24" } + iam = { + "roles/compute.networkUser" = [ + local.service_project_1.cloud_services_service_account, + local.service_project_1.gke_service_account + ] + "roles/compute.securityAdmin" = [ + local.service_project_1.gke_service_account + ] + } } ] shared_vpc_host = true @@ -219,17 +225,6 @@ module "vpc-host" { local.service_project_1.project_id, local.service_project_2.project_id ] - subnet_iam = { - "europe-west1/subnet-1" = { - "roles/compute.networkUser" = [ - local.service_project_1.cloud_services_service_account, - local.service_project_1.gke_service_account - ] - "roles/compute.securityAdmin" = [ - local.service_project_1.gke_service_account - ] - } - } } # tftest modules=1 resources=9 inventory=shared-vpc.yaml ``` @@ -299,6 +294,13 @@ module "vpc" { name = "regional-proxy" region = "europe-west1" active = true + }, + { + ip_cidr_range = "10.0.4.0/24" + name = "global-proxy" + region = "australia-southeast2" + active = true + global = true } ] subnets_psc = [ @@ -309,7 +311,7 @@ module "vpc" { } ] } -# tftest modules=1 resources=5 inventory=proxy-only-subnets.yaml +# tftest modules=1 resources=6 inventory=proxy-only-subnets.yaml ``` ### DNS Policies @@ -343,12 +345,14 @@ The `net-vpc` module includes a subnet factory (see [Resource Factories](../../b ```hcl module "vpc" { - source = "./fabric/modules/net-vpc" - project_id = "my-project" - name = "my-network" - data_folder = "config/subnets" + source = "./fabric/modules/net-vpc" + project_id = "my-project" + name = "my-network" + factories_config = { + subnets_folder = "config/subnets" + } } -# tftest modules=1 resources=9 files=subnet-simple,subnet-simple-2,subnet-detailed,subnet-proxy,subnet-psc inventory=factory.yaml +# tftest modules=1 resources=10 files=subnet-simple,subnet-simple-2,subnet-detailed,subnet-proxy,subnet-proxy-global,subnet-psc inventory=factory.yaml ``` ```yaml @@ -372,31 +376,39 @@ description: Sample description ip_cidr_range: 10.0.0.0/24 # optional attributes enable_private_access: false # defaults to true -iam: # grant roles/compute.networkUser - - group:lorem@example.com - - serviceAccount:fbz@prj.iam.gserviceaccount.com - - user:foobar@example.com +iam: + roles/compute.networkUser: + - group:lorem@example.com + - serviceAccount:fbz@prj.iam.gserviceaccount.com + - user:foobar@example.com secondary_ip_ranges: # map of secondary ip ranges secondary-range-a: 192.168.0.0/24 -flow_logs: # enable, set to empty map to use defaults +flow_logs_config: # enable, set to empty map to use defaults aggregation_interval: "INTERVAL_5_SEC" flow_sampling: 0.5 metadata: "INCLUDE_ALL_METADATA" - filter_expression: null ``` ```yaml # tftest-file id=subnet-proxy path=config/subnets/subnet-proxy.yaml region: europe-west4 ip_cidr_range: 10.1.0.0/24 -purpose: REGIONAL_MANAGED_PROXY +proxy_only: true +``` + +```yaml +# tftest-file id=subnet-proxy-global path=config/subnets/subnet-proxy-global.yaml +region: australia-southeast2 +ip_cidr_range: 10.4.0.0/24 +proxy_only: true +global: true ``` ```yaml # tftest-file id=subnet-psc path=config/subnets/subnet-psc.yaml region: europe-west4 ip_cidr_range: 10.2.0.0/24 -purpose: PRIVATE_SERVICE_CONNECT +psc: true ``` ### Custom Routes @@ -525,30 +537,27 @@ module "vpc" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L93) | The name of the network being created. | string | ✓ | | -| [project_id](variables.tf#L109) | The ID of the project where this VPC will be created. | string | ✓ | | +| [name](variables.tf#L95) | The name of the network being created. | string | ✓ | | +| [project_id](variables.tf#L111) | The ID of the project where this VPC will be created. | string | ✓ | | | [auto_create_subnetworks](variables.tf#L17) | Set to true to create an auto mode subnet, defaults to custom mode. | bool | | false | | [create_googleapis_routes](variables.tf#L23) | Toggle creation of googleapis private/restricted routes. Disabled when vpc creation is turned off, or when set to null. | object({…}) | | {} | -| [data_folder](variables.tf#L34) | An optional folder containing the subnet configurations in YaML format. | string | | null | -| [delete_default_routes_on_create](variables.tf#L40) | Set to true to delete the default routes at creation time. | bool | | false | -| [description](variables.tf#L46) | An optional description of this resource (triggers recreation on change). | string | | "Terraform-managed." | -| [dns_policy](variables.tf#L52) | DNS policy setup for the VPC. | object({…}) | | null | -| [firewall_policy_enforcement_order](variables.tf#L65) | Order that Firewall Rules and Firewall Policies are evaluated. Can be either 'BEFORE_CLASSIC_FIREWALL' or 'AFTER_CLASSIC_FIREWALL'. | string | | "AFTER_CLASSIC_FIREWALL" | -| [ipv6_config](variables.tf#L77) | Optional IPv6 configuration for this network. | object({…}) | | {} | -| [mtu](variables.tf#L87) | Maximum Transmission Unit in bytes. The minimum value for this field is 1460 (the default) and the maximum value is 1500 bytes. | number | | null | -| [peering_config](variables.tf#L98) | VPC peering configuration. | object({…}) | | null | -| [psa_config](variables.tf#L114) | The Private Service Access configuration for Service Networking. | object({…}) | | null | -| [routes](variables.tf#L124) | Network routes, keyed by name. | map(object({…})) | | {} | -| [routing_mode](variables.tf#L145) | The network routing mode (default 'GLOBAL'). | string | | "GLOBAL" | -| [shared_vpc_host](variables.tf#L155) | Enable shared VPC for this project. | bool | | false | -| [shared_vpc_service_projects](variables.tf#L161) | Shared VPC service projects to register with this host. | list(string) | | [] | -| [subnet_iam](variables.tf#L167) | Subnet IAM bindings in {REGION/NAME => {ROLE => [MEMBERS]} format. | map(map(list(string))) | | {} | -| [subnet_iam_bindings](variables.tf#L173) | Authoritative IAM bindings in {REGION/NAME => {ROLE => {members = [], condition = {}}}}. | map(map(object({…}))) | | {} | -| [subnet_iam_bindings_additive](variables.tf#L187) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [subnets](variables.tf#L203) | Subnet configuration. | list(object({…})) | | [] | -| [subnets_proxy_only](variables.tf#L229) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…})) | | [] | -| [subnets_psc](variables.tf#L241) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | -| [vpc_create](variables.tf#L252) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true | +| [delete_default_routes_on_create](variables.tf#L34) | Set to true to delete the default routes at creation time. | bool | | false | +| [description](variables.tf#L40) | An optional description of this resource (triggers recreation on change). | string | | "Terraform-managed." | +| [dns_policy](variables.tf#L46) | DNS policy setup for the VPC. | object({…}) | | null | +| [factories_config](variables.tf#L59) | Paths to data files and folders that enable factory functionality. | object({…}) | | null | +| [firewall_policy_enforcement_order](variables.tf#L67) | Order that Firewall Rules and Firewall Policies are evaluated. Can be either 'BEFORE_CLASSIC_FIREWALL' or 'AFTER_CLASSIC_FIREWALL'. | string | | "AFTER_CLASSIC_FIREWALL" | +| [ipv6_config](variables.tf#L79) | Optional IPv6 configuration for this network. | object({…}) | | {} | +| [mtu](variables.tf#L89) | Maximum Transmission Unit in bytes. The minimum value for this field is 1460 (the default) and the maximum value is 1500 bytes. | number | | null | +| [peering_config](variables.tf#L100) | VPC peering configuration. | object({…}) | | null | +| [psa_config](variables.tf#L116) | The Private Service Access configuration for Service Networking. | object({…}) | | null | +| [routes](variables.tf#L126) | Network routes, keyed by name. | map(object({…})) | | {} | +| [routing_mode](variables.tf#L147) | The network routing mode (default 'GLOBAL'). | string | | "GLOBAL" | +| [shared_vpc_host](variables.tf#L157) | Enable shared VPC for this project. | bool | | false | +| [shared_vpc_service_projects](variables.tf#L163) | Shared VPC service projects to register with this host. | list(string) | | [] | +| [subnets](variables.tf#L169) | Subnet configuration. | list(object({…})) | | [] | +| [subnets_proxy_only](variables.tf#L216) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…})) | | [] | +| [subnets_psc](variables.tf#L250) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | +| [vpc_create](variables.tf#L282) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true | ## Outputs diff --git a/modules/net-vpc/outputs.tf b/modules/net-vpc/outputs.tf index fbf07dba..503923d9 100644 --- a/modules/net-vpc/outputs.tf +++ b/modules/net-vpc/outputs.tf @@ -136,4 +136,4 @@ output "subnets_proxy_only" { output "subnets_psc" { description = "Private Service Connect subnet resources." value = { for k, v in google_compute_subnetwork.psc : k => v } -} +} \ No newline at end of file diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf index 0e656fd8..fe5abea9 100644 --- a/modules/net-vpc/subnets.tf +++ b/modules/net-vpc/subnets.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. @@ -18,66 +18,114 @@ locals { _factory_data = { - for f in try(fileset(var.data_folder, "**/*.yaml"), []) : - trimsuffix(basename(f), ".yaml") => yamldecode(file("${var.data_folder}/${f}")) + for f in try(fileset(var.factories_config.subnets_folder, "**/*.yaml"), []) : + trimsuffix(basename(f), ".yaml") => yamldecode(file("${var.factories_config.subnets_folder}/${f}")) } _factory_subnets = { - for k, v in local._factory_data : "${v.region}/${try(v.name, k)}" => { - name = try(v.name, k) - ip_cidr_range = v.ip_cidr_range - region = v.region + for k, v in local._factory_data : + "${v.region}/${try(v.name, k)}" => { + active = try(v.active, true) description = try(v.description, null) enable_private_access = try(v.enable_private_access, true) - flow_logs_config = try(v.flow_logs, null) - ipv6 = try(v.ipv6, null) - secondary_ip_ranges = try(v.secondary_ip_ranges, null) - iam = try(v.iam, []) - iam_members = try(v.iam_members, []) - purpose = try(v.purpose, null) - active = try(v.active, null) + flow_logs_config = can(v.flow_logs_config) ? { + aggregation_interval = try(v.flow_logs_config.aggregation_interval, null) + filter_expression = try(v.flow_logs_config.filter_expression, null) + flow_sampling = try(v.flow_logs_config.flow_sampling, null) + metadata = try(v.flow_logs_config.metadata, null) + metadata_fields = try(v.flow_logs_config.metadata_fields, null) + } : null + global = try(v.global, false) + ip_cidr_range = v.ip_cidr_range + ipv6 = !can(v.ipv6) ? null : { + access_type = try(v.ipv6.access_type, "INTERNAL") + } + name = try(v.name, k) + region = v.region + secondary_ip_ranges = try(v.secondary_ip_ranges, null) + iam = try(v.iam, {}) + iam_bindings = !can(v.iam_bindings) ? {} : { + for k2, v2 in v.iam_bindings : + k2 => { + role = v2.role + members = v2.members + condition = !can(v2.condition) ? null : { + expression = v2.condition.expression + title = v2.condition.title + description = try(v2.condition.description, null) + } + } + } + iam_bindings_additive = !can(v.iam_bindings_additive) ? {} : { + for k2, v2 in v.iam_bindings_additive : + k2 => { + member = v2.member + role = v2.role + condition = !can(v2.condition) ? null : { + expression = v2.condition.expression + title = v2.condition.title + description = try(v2.condition.description, null) + } + } + } + _is_regular = !try(v.psc == true, false) && !try(v.proxy_only == true, false) + _is_psc = try(v.psc == true, false) + _is_proxy_only = try(v.proxy_only == true, false) } } - _factory_subnets_iam = [ - for k, v in local._factory_subnets : { - subnet = k - role = "roles/compute.networkUser" - members = v.iam - } if v.purpose == null && v.iam != null - ] - _subnet_iam = flatten([ - for subnet, roles in(var.subnet_iam == null ? {} : var.subnet_iam) : [ - for role, members in roles : { - members = members - role = role - subnet = subnet - } - ] - ]) - subnet_iam = concat( - [for k in local._factory_subnets_iam : k if length(k.members) > 0], - local._subnet_iam + + all_subnets = merge( + { for k, v in google_compute_subnetwork.subnetwork : k => v }, + { for k, v in google_compute_subnetwork.proxy_only : k => v }, + { for k, v in google_compute_subnetwork.psc : k => v } ) - subnet_iam_bindings = flatten([ - for subnet, roles in(var.subnet_iam_bindings == null ? {} : var.subnet_iam_bindings) : [ - for role, data in roles : { - role = role - subnet = subnet + subnet_iam = flatten(concat( + [ + for s in concat(var.subnets, var.subnets_psc, var.subnets_proxy_only, values(local._factory_subnets)) : [ + for role, members in s.iam : + { + role = role + members = members + subnet = "${s.region}/${s.name}" + } + ] + ], + )) + subnet_iam_bindings = merge([ + for s in concat(var.subnets, var.subnets_psc, var.subnets_proxy_only, values(local._factory_subnets)) : { + for key, data in s.iam_bindings : + key => { + role = data.role + subnet = "${s.region}/${s.name}" members = data.members condition = data.condition } - ] - ]) + } + ]...) + # note: all additive bindings share a single namespace for the key. + # In other words, if you have multiple additive bindings with the + # same name, only one will be used + subnet_iam_bindings_additive = merge([ + for s in concat(var.subnets, var.subnets_psc, var.subnets_proxy_only, values(local._factory_subnets)) : { + for key, data in s.iam_bindings_additive : + key => { + role = data.role + subnet = "${s.region}/${s.name}" + member = data.member + condition = data.condition + } + } + ]...) subnets = merge( { for s in var.subnets : "${s.region}/${s.name}" => s }, - { for k, v in local._factory_subnets : k => v if v.purpose == null } + { for k, v in local._factory_subnets : k => v if v._is_regular } ) subnets_proxy_only = merge( { for s in var.subnets_proxy_only : "${s.region}/${s.name}" => s }, - { for k, v in local._factory_subnets : k => v if v.purpose == "REGIONAL_MANAGED_PROXY" } + { for k, v in local._factory_subnets : k => v if v._is_proxy_only }, ) subnets_psc = merge( { for s in var.subnets_psc : "${s.region}/${s.name}" => s }, - { for k, v in local._factory_subnets : k => v if v.purpose == "PRIVATE_SERVICE_CONNECT" } + { for k, v in local._factory_subnets : k => v if v._is_psc } ) } @@ -128,13 +176,12 @@ resource "google_compute_subnetwork" "proxy_only" { name = each.value.name region = each.value.region ip_cidr_range = each.value.ip_cidr_range - description = ( - each.value.description == null - ? "Terraform-managed proxy-only subnet for Regional HTTPS or Internal HTTPS LB." - : each.value.description + description = coalesce( + each.value.description, + "Terraform-managed proxy-only subnet for Regional HTTPS, Internal HTTPS or Cross-Regional HTTPS Internal LB." ) - purpose = "REGIONAL_MANAGED_PROXY" - role = each.value.active != false ? "ACTIVE" : "BACKUP" + purpose = each.value.global ? "GLOBAL_MANAGED_PROXY" : "REGIONAL_MANAGED_PROXY" + role = each.value.active ? "ACTIVE" : "BACKUP" } resource "google_compute_subnetwork" "psc" { @@ -144,34 +191,31 @@ resource "google_compute_subnetwork" "psc" { name = each.value.name region = each.value.region ip_cidr_range = each.value.ip_cidr_range - description = ( - each.value.description == null - ? "Terraform-managed subnet for Private Service Connect (PSC NAT)." - : each.value.description + description = coalesce( + each.value.description, + "Terraform-managed subnet for Private Service Connect (PSC NAT)." ) purpose = "PRIVATE_SERVICE_CONNECT" } + resource "google_compute_subnetwork_iam_binding" "authoritative" { for_each = { for binding in local.subnet_iam : "${binding.subnet}.${binding.role}" => binding } project = var.project_id - subnetwork = google_compute_subnetwork.subnetwork[each.value.subnet].name - region = google_compute_subnetwork.subnetwork[each.value.subnet].region + subnetwork = local.all_subnets[each.value.subnet].name + region = local.all_subnets[each.value.subnet].region role = each.value.role members = each.value.members } resource "google_compute_subnetwork_iam_binding" "bindings" { - for_each = { - for binding in local.subnet_iam_bindings : - "${binding.subnet}.${binding.role}.${try(binding.condition.title, "")}" => binding - } + for_each = local.subnet_iam_bindings project = var.project_id - subnetwork = google_compute_subnetwork.subnetwork[each.value.subnet].name - region = google_compute_subnetwork.subnetwork[each.value.subnet].region + subnetwork = local.all_subnets[each.value.subnet].name + region = local.all_subnets[each.value.subnet].region role = each.value.role members = each.value.members dynamic "condition" { @@ -184,13 +228,11 @@ resource "google_compute_subnetwork_iam_binding" "bindings" { } } -# TODO: merge factory subnet IAM members - resource "google_compute_subnetwork_iam_member" "bindings" { - for_each = var.subnet_iam_bindings_additive + for_each = local.subnet_iam_bindings_additive project = var.project_id - subnetwork = google_compute_subnetwork.subnetwork[each.value.subnet].name - region = google_compute_subnetwork.subnetwork[each.value.subnet].region + subnetwork = local.all_subnets[each.value.subnet].name + region = local.all_subnets[each.value.subnet].region role = each.value.role member = each.value.member dynamic "condition" { diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf index 3837c9b0..5c4cc692 100644 --- a/modules/net-vpc/variables.tf +++ b/modules/net-vpc/variables.tf @@ -31,12 +31,6 @@ variable "create_googleapis_routes" { default = {} } -variable "data_folder" { - description = "An optional folder containing the subnet configurations in YaML format." - type = string - default = null -} - variable "delete_default_routes_on_create" { description = "Set to true to delete the default routes at creation time." type = bool @@ -62,6 +56,14 @@ variable "dns_policy" { default = null } +variable "factories_config" { + description = "Paths to data files and folders that enable factory functionality." + type = object({ + subnets_folder = string + }) + default = null +} + variable "firewall_policy_enforcement_order" { description = "Order that Firewall Rules and Firewall Policies are evaluated. Can be either 'BEFORE_CLASSIC_FIREWALL' or 'AFTER_CLASSIC_FIREWALL'." type = string @@ -164,42 +166,6 @@ variable "shared_vpc_service_projects" { default = [] } -variable "subnet_iam" { - description = "Subnet IAM bindings in {REGION/NAME => {ROLE => [MEMBERS]} format." - type = map(map(list(string))) - default = {} -} - -variable "subnet_iam_bindings" { - description = "Authoritative IAM bindings in {REGION/NAME => {ROLE => {members = [], condition = {}}}}." - type = map(map(object({ - members = list(string) - condition = optional(object({ - expression = string - title = string - description = optional(string) - })) - }))) - nullable = false - default = {} -} - -variable "subnet_iam_bindings_additive" { - description = "Individual additive IAM bindings. Keys are arbitrary." - type = map(object({ - member = string - role = string - subnet = string - condition = optional(object({ - expression = string - title = string - description = optional(string) - })) - })) - nullable = false - default = {} -} - variable "subnets" { description = "Subnet configuration." type = list(object({ @@ -222,20 +188,63 @@ variable "subnets" { # enable_private_access = optional(string) })) secondary_ip_ranges = optional(map(string)) + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + role = string + members = list(string) + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })) - default = [] + default = [] + nullable = false } variable "subnets_proxy_only" { - description = "List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active." + description = "List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active." type = list(object({ name = string ip_cidr_range = string region = string description = optional(string) - active = bool + active = optional(bool, true) + global = optional(bool, false) + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + role = string + members = list(string) + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })) - default = [] + default = [] + nullable = false } variable "subnets_psc" { @@ -245,8 +254,29 @@ variable "subnets_psc" { ip_cidr_range = string region = string description = optional(string) + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + role = string + members = list(string) + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })) - default = [] + default = [] + nullable = false } variable "vpc_create" { diff --git a/modules/net-vpc/versions.tf b/modules/net-vpc/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-vpc/versions.tf +++ b/modules/net-vpc/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-vpn-dynamic/versions.tf b/modules/net-vpn-dynamic/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-vpn-dynamic/versions.tf +++ b/modules/net-vpn-dynamic/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-vpn-ha/versions.tf b/modules/net-vpn-ha/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-vpn-ha/versions.tf +++ b/modules/net-vpn-ha/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/net-vpn-static/versions.tf b/modules/net-vpn-static/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/net-vpn-static/versions.tf +++ b/modules/net-vpn-static/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/organization/README.md b/modules/organization/README.md index eb228dcf..fd9ca094 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -446,24 +446,24 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L210) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L211) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | | [firewall_policy](variables.tf#L31) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | | [group_iam](variables.tf#L40) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L47) | IAM bindings, in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L54) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L68) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [logging_data_access](variables.tf#L83) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L98) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L105) | Logging sinks to create for the organization. | map(object({…})) | | {} | -| [network_tags](variables.tf#L135) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L157) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L184) | Path containing org policies in YAML format. | string | | null | -| [org_policy_custom_constraints](variables.tf#L190) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | -| [org_policy_custom_constraints_data_path](variables.tf#L204) | Path containing org policy custom constraints in YAML format. | string | | null | -| [tag_bindings](variables.tf#L219) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L225) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [iam_bindings](variables.tf#L54) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L69) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [logging_data_access](variables.tf#L84) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables.tf#L99) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L106) | Logging sinks to create for the organization. | map(object({…})) | | {} | +| [network_tags](variables.tf#L136) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [org_policies](variables.tf#L158) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policies_data_path](variables.tf#L185) | Path containing org policies in YAML format. | string | | null | +| [org_policy_custom_constraints](variables.tf#L191) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policy_custom_constraints_data_path](variables.tf#L205) | Path containing org policy custom constraints in YAML format. | string | | null | +| [tag_bindings](variables.tf#L220) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L226) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | ## Outputs diff --git a/modules/organization/iam.tf b/modules/organization/iam.tf index 2882d02a..81a8d2b0 100644 --- a/modules/organization/iam.tf +++ b/modules/organization/iam.tf @@ -51,7 +51,7 @@ resource "google_organization_iam_binding" "authoritative" { resource "google_organization_iam_binding" "bindings" { for_each = var.iam_bindings org_id = local.organization_id_numeric - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 99fe49c6..c9899e2e 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -52,9 +52,10 @@ variable "iam" { } variable "iam_bindings" { - description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/organization/versions.tf b/modules/organization/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/organization/versions.tf +++ b/modules/organization/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/project/README.md b/modules/project/README.md index 7479eca8..3fddf95f 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -117,10 +117,11 @@ module "project" { "stackdriver.googleapis.com" ] iam_bindings = { - "roles/resourcemanager.projectIamAdmin" = { + iam_admin_conditional = { members = [ "group:test-admins@example.org" ] + role = "roles/resourcemanager.projectIamAdmin" condition = { title = "delegated_network_user_one" expression = <<-END @@ -589,7 +590,7 @@ output "compute_robot" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L185) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L186) | Project name and id suffix. | string | ✓ | | | [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | | [billing_account](variables.tf#L23) | Billing account id. | string | | null | | [compute_metadata](variables.tf#L29) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} | @@ -599,28 +600,28 @@ output "compute_robot" { | [descriptive_name](variables.tf#L63) | Name of the project name. Used for project name instead of `name` variable. | string | | null | | [group_iam](variables.tf#L69) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L76) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L83) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L97) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [labels](variables.tf#L112) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L119) | If non-empty, creates a project lien with this description. | string | | null | -| [logging_data_access](variables.tf#L125) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | -| [logging_exclusions](variables.tf#L140) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | -| [logging_sinks](variables.tf#L147) | Logging sinks to create for this project. | map(object({…})) | | {} | -| [metric_scopes](variables.tf#L178) | List of projects that will act as metric scopes for this project. | list(string) | | [] | -| [org_policies](variables.tf#L190) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | -| [org_policies_data_path](variables.tf#L217) | Path containing org policies in YAML format. | string | | null | -| [parent](variables.tf#L223) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L233) | Optional prefix used to generate project id and name. | string | | null | -| [project_create](variables.tf#L243) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | -| [service_config](variables.tf#L249) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L261) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | -| [service_perimeter_bridges](variables.tf#L268) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | -| [service_perimeter_standard](variables.tf#L275) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | -| [services](variables.tf#L281) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L287) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L296) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L318) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | -| [tag_bindings](variables.tf#L324) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | +| [iam_bindings](variables.tf#L83) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L98) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [labels](variables.tf#L113) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L120) | If non-empty, creates a project lien with this description. | string | | null | +| [logging_data_access](variables.tf#L126) | Control activation of data access logs. Format is service => { log type => [exempted members]}. The special 'allServices' key denotes configuration for all services. | map(map(list(string))) | | {} | +| [logging_exclusions](variables.tf#L141) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | +| [logging_sinks](variables.tf#L148) | Logging sinks to create for this project. | map(object({…})) | | {} | +| [metric_scopes](variables.tf#L179) | List of projects that will act as metric scopes for this project. | list(string) | | [] | +| [org_policies](variables.tf#L191) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [org_policies_data_path](variables.tf#L218) | Path containing org policies in YAML format. | string | | null | +| [parent](variables.tf#L224) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L234) | Optional prefix used to generate project id and name. | string | | null | +| [project_create](variables.tf#L244) | Create project. When set to false, uses a data source to reference existing project. | bool | | true | +| [service_config](variables.tf#L250) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L262) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string)) | | {} | +| [service_perimeter_bridges](variables.tf#L269) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string) | | null | +| [service_perimeter_standard](variables.tf#L276) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string | | null | +| [services](variables.tf#L282) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L288) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L297) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L319) | Allows the underlying resources to be destroyed without destroying the project itself. | bool | | false | +| [tag_bindings](variables.tf#L325) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/project/iam.tf b/modules/project/iam.tf index 16f187d6..0f00f286 100644 --- a/modules/project/iam.tf +++ b/modules/project/iam.tf @@ -58,7 +58,7 @@ resource "google_project_iam_binding" "authoritative" { resource "google_project_iam_binding" "bindings" { for_each = var.iam_bindings project = local.project.project_id - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/project/service-agents.yaml b/modules/project/service-agents.yaml index 4ef3cafd..c8eff2df 100644 --- a/modules/project/service-agents.yaml +++ b/modules/project/service-agents.yaml @@ -221,6 +221,7 @@ service_agent: "service-%s@gcp-sa-healthcare.iam.gserviceaccount.com" - name: "iap" service_agent: "service-%s@gcp-sa-iap.iam.gserviceaccount.com" + jit: true - name: "identitytoolkit" service_agent: "service-%s@gcp-sa-identitytoolkit.iam.gserviceaccount.com" - name: "ids" diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 2824fcf3..68f8b6c0 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -81,9 +81,10 @@ variable "iam" { } variable "iam_bindings" { - description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/project/versions.tf b/modules/project/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/project/versions.tf +++ b/modules/project/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/projects-data-source/versions.tf b/modules/projects-data-source/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/projects-data-source/versions.tf +++ b/modules/projects-data-source/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/pubsub/README.md b/modules/pubsub/README.md index 44a0e737..69a18dbe 100644 --- a/modules/pubsub/README.md +++ b/modules/pubsub/README.md @@ -61,16 +61,10 @@ module "pubsub" { project_id = "my-project" name = "my-topic" subscriptions = { - test-pull = null + test-pull = {} test-pull-override = { - labels = { test = "override" } - options = { - ack_deadline_seconds = null - message_retention_duration = null - retain_acked_messages = true - expiration_policy_ttl = null - filter = null - } + labels = { test = "override" } + retain_acked_messages = true } } } @@ -87,13 +81,10 @@ module "pubsub" { project_id = "my-project" name = "my-topic" subscriptions = { - test-push = null - } - push_configs = { test-push = { - endpoint = "https://example.com/foo" - attributes = null - oidc_token = null + push = { + endpoint = "https://example.com/foo" + } } } } @@ -110,20 +101,45 @@ module "pubsub" { project_id = "my-project" name = "my-topic" subscriptions = { - test-bigquery = null - } - bigquery_subscription_configs = { test-bigquery = { - table = "my_project_id:my_dataset.my_table" - use_topic_schema = true - write_metadata = false - drop_unknown_fields = true + bigquery = { + table = "my_project_id:my_dataset.my_table" + use_topic_schema = true + write_metadata = false + drop_unknown_fields = true + } } } } # tftest modules=1 resources=2 ``` +### Cloud Storage subscriptions + +Cloud Storage subscriptions need extra configuration in the `cloud_storage_subscription_configs` variable. + +```hcl +module "pubsub" { + source = "./fabric/modules/pubsub" + project_id = "my-project" + name = "my-topic" + subscriptions = { + test-cloudstorage = { + cloud_storage = { + bucket = "my-bucket" + filename_prefix = "test_prefix" + filename_suffix = "test_suffix" + max_duration = "100s" + max_bytes = 1000 + avro_config = { + write_metadata = true + } + } + } + } +} +# tftest modules=1 resources=2 +``` ### Subscriptions with IAM ```hcl @@ -132,47 +148,40 @@ module "pubsub" { project_id = "my-project" name = "my-topic" subscriptions = { - test-1 = null - test-1 = null - } - subscription_iam = { test-1 = { - "roles/pubsub.subscriber" = ["user:user1@ludomagno.net"] + iam = { + "roles/pubsub.subscriber" = ["user:user1@example.com"] + } } } } # tftest modules=1 resources=3 ``` - ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L79) | PubSub topic name. | string | ✓ | | -| [project_id](variables.tf#L84) | Project used for resources. | string | ✓ | | -| [bigquery_subscription_configs](variables.tf#L17) | Configuration parameters for BigQuery subscriptions. | map(object({…})) | | {} | -| [dead_letter_configs](variables.tf#L28) | Per-subscription dead letter policy configuration. | map(object({…})) | | {} | -| [defaults](variables.tf#L37) | Subscription defaults for options. | object({…}) | | {…} | -| [iam](variables.tf#L55) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [kms_key](variables.tf#L61) | KMS customer managed encryption key. | string | | null | -| [labels](variables.tf#L67) | Labels. | map(string) | | {} | -| [message_retention_duration](variables.tf#L73) | Minimum duration to retain a message after it is published to the topic. | string | | null | -| [push_configs](variables.tf#L89) | Push subscription configurations. | map(object({…})) | | {} | -| [regions](variables.tf#L102) | List of regions used to set persistence policy. | list(string) | | [] | -| [schema](variables.tf#L108) | Topic schema. If set, all messages in this topic should follow this schema. | object({…}) | | null | -| [subscription_iam](variables.tf#L118) | IAM bindings for subscriptions in {SUBSCRIPTION => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {} | -| [subscriptions](variables.tf#L124) | Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null. | map(object({…})) | | {} | +| [name](variables.tf#L73) | PubSub topic name. | string | ✓ | | +| [project_id](variables.tf#L78) | Project used for resources. | string | ✓ | | +| [iam](variables.tf#L17) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L39) | Keyring individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [kms_key](variables.tf#L54) | KMS customer managed encryption key. | string | | null | +| [labels](variables.tf#L60) | Labels. | map(string) | | {} | +| [message_retention_duration](variables.tf#L67) | Minimum duration to retain a message after it is published to the topic. | string | | null | +| [regions](variables.tf#L83) | List of regions used to set persistence policy. | list(string) | | [] | +| [schema](variables.tf#L90) | Topic schema. If set, all messages in this topic should follow this schema. | object({…}) | | null | +| [subscriptions](variables.tf#L100) | Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null. | map(object({…})) | | {} | ## Outputs | name | description | sensitive | |---|---|:---:| | [id](outputs.tf#L17) | Fully qualified topic id. | | -| [schema](outputs.tf#L26) | Schema resource. | | -| [schema_id](outputs.tf#L31) | Schema resource id. | | -| [subscription_id](outputs.tf#L36) | Subscription ids. | | -| [subscriptions](outputs.tf#L46) | Subscription resources. | | -| [topic](outputs.tf#L54) | Topic resource. | | - +| [schema](outputs.tf#L27) | Schema resource. | | +| [schema_id](outputs.tf#L32) | Schema resource id. | | +| [subscription_id](outputs.tf#L37) | Subscription ids. | | +| [subscriptions](outputs.tf#L48) | Subscription resources. | | +| [topic](outputs.tf#L57) | Topic resource. | | diff --git a/modules/pubsub/iam.tf b/modules/pubsub/iam.tf new file mode 100644 index 00000000..4e39b43a --- /dev/null +++ b/modules/pubsub/iam.tf @@ -0,0 +1,140 @@ +/** + * 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 authoritative. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + subscription_iam = flatten([ + for k, v in var.subscriptions : [ + for role, members in v.iam : { + subscription = k + role = role + members = members + } + ] + ]) + subscription_iam_bindings = merge([ + for k, v in var.subscriptions : { + for binding_key, data in v.iam_bindings : + binding_key => { + subscription = k + role = data.role + members = data.members + condition = data.condition + } + } + ]...) + subscription_iam_bindings_additive = merge([ + for k, v in var.subscriptions : { + for binding_key, data in v.iam_bindings_additive : + binding_key => { + subscription = k + role = data.role + member = data.member + condition = data.condition + } + } + ]...) +} + +moved { + from = google_pubsub_topic_iam_binding.default + to = google_pubsub_topic_iam_binding.authoritative +} + +resource "google_pubsub_topic_iam_binding" "authoritative" { + for_each = var.iam + project = var.project_id + topic = google_pubsub_topic.default.name + role = each.key + members = each.value +} + +resource "google_pubsub_topic_iam_binding" "bindings" { + for_each = var.iam_bindings + topic = google_pubsub_topic.default.name + role = each.value.role + members = each.value.members + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_pubsub_topic_iam_member" "bindings" { + for_each = var.iam_bindings_additive + topic = google_pubsub_topic.default.name + role = each.value.role + member = each.value.member + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +moved { + from = google_pubsub_subscription_iam_binding.default + to = google_pubsub_subscription_iam_binding.authoritative +} + +resource "google_pubsub_subscription_iam_binding" "authoritative" { + for_each = { + for binding in local.subscription_iam : + "${binding.subscription}.${binding.role}" => binding + } + project = var.project_id + subscription = google_pubsub_subscription.default[each.value.subscription].name + role = each.value.role + members = each.value.members +} + +resource "google_pubsub_subscription_iam_binding" "bindings" { + for_each = local.subscription_iam_bindings + project = var.project_id + subscription = google_pubsub_subscription.default[each.value.subscription].name + role = each.value.role + members = each.value.members + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_pubsub_subscription_iam_member" "members" { + for_each = local.subscription_iam_bindings_additive + project = var.project_id + subscription = google_pubsub_subscription.default[each.value.subscription].name + role = each.value.role + member = each.value.member + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} diff --git a/modules/pubsub/main.tf b/modules/pubsub/main.tf index ccb6f5d7..de065029 100644 --- a/modules/pubsub/main.tf +++ b/modules/pubsub/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. @@ -15,24 +15,6 @@ */ locals { - sub_iam_members = flatten([ - for sub, roles in var.subscription_iam : [ - for role, members in roles : { - sub = sub - role = role - members = members - } - ] - ]) - oidc_config = { - for k, v in var.push_configs : k => v.oidc_token - } - subscriptions = { - for k, v in var.subscriptions : k => { - labels = try(v.labels, v, null) == null ? var.labels : v.labels - options = try(v.options, v, null) == null ? var.defaults : v.options - } - } topic_id_static = "projects/${var.project_id}/topics/${var.name}" } @@ -67,75 +49,73 @@ resource "google_pubsub_topic" "default" { } } -resource "google_pubsub_topic_iam_binding" "default" { - for_each = var.iam - project = var.project_id - topic = google_pubsub_topic.default.name - role = each.key - members = each.value -} - resource "google_pubsub_subscription" "default" { - for_each = local.subscriptions - project = var.project_id - name = each.key - topic = google_pubsub_topic.default.name - labels = each.value.labels - ack_deadline_seconds = each.value.options.ack_deadline_seconds - message_retention_duration = each.value.options.message_retention_duration - retain_acked_messages = each.value.options.retain_acked_messages - filter = each.value.options.filter + for_each = var.subscriptions + project = var.project_id + name = each.key + topic = google_pubsub_topic.default.name + labels = each.value.labels + ack_deadline_seconds = each.value.ack_deadline_seconds + message_retention_duration = each.value.message_retention_duration + retain_acked_messages = each.value.retain_acked_messages + filter = each.value.filter + enable_message_ordering = each.value.enable_message_ordering + enable_exactly_once_delivery = each.value.enable_exactly_once_delivery dynamic "expiration_policy" { - for_each = each.value.options.expiration_policy_ttl == null ? [] : [""] + for_each = each.value.expiration_policy_ttl == null ? [] : [""] content { - ttl = each.value.options.expiration_policy_ttl + ttl = each.value.expiration_policy_ttl } } dynamic "dead_letter_policy" { - for_each = try(var.dead_letter_configs[each.key], null) == null ? [] : [""] + for_each = each.value.dead_letter_policy == null ? [] : [""] content { - dead_letter_topic = var.dead_letter_configs[each.key].topic - max_delivery_attempts = var.dead_letter_configs[each.key].max_delivery_attempts + dead_letter_topic = each.value.dead_letter_policy.topic + max_delivery_attempts = each.value.dead_letter_policy.max_delivery_attempts } } dynamic "push_config" { - for_each = try(var.push_configs[each.key], null) == null ? [] : [""] + for_each = each.value.push == null ? [] : [""] content { - push_endpoint = var.push_configs[each.key].endpoint - attributes = var.push_configs[each.key].attributes + push_endpoint = each.value.push.endpoint + attributes = each.value.push.attributes dynamic "oidc_token" { - for_each = ( - local.oidc_config[each.key] == null ? [] : [""] - ) + for_each = each.value.push.oidc_token == null ? [] : [""] content { - service_account_email = local.oidc_config[each.key].service_account_email - audience = local.oidc_config[each.key].audience + service_account_email = each.value.push.oidc_token.service_account_email + audience = each.value.push.oidc_token.audience } } } } dynamic "bigquery_config" { - for_each = try(var.bigquery_subscription_configs[each.key], null) == null ? [] : [""] + for_each = each.value.bigquery == null ? [] : [""] content { - table = var.bigquery_subscription_configs[each.key].table - use_topic_schema = var.bigquery_subscription_configs[each.key].use_topic_schema - write_metadata = var.bigquery_subscription_configs[each.key].write_metadata - drop_unknown_fields = var.bigquery_subscription_configs[each.key].drop_unknown_fields + table = each.value.bigquery.table + use_topic_schema = each.value.bigquery.use_topic_schema + write_metadata = each.value.bigquery.write_metadata + drop_unknown_fields = each.value.bigquery.drop_unknown_fields + } + } + + dynamic "cloud_storage_config" { + for_each = each.value.cloud_storage == null ? [] : [""] + content { + bucket = each.value.cloud_storage.bucket + filename_prefix = each.value.cloud_storage.filename_prefix + filename_suffix = each.value.cloud_storage.filename_suffix + max_duration = each.value.cloud_storage.max_duration + max_bytes = each.value.cloud_storage.max_bytes + dynamic "avro_config" { + for_each = each.value.cloud_storage.avro_config == null ? [] : [""] + content { + write_metadata = each.value.cloud_storage.avro_config.write_metadata + } + } } } } - -resource "google_pubsub_subscription_iam_binding" "default" { - for_each = { - for binding in local.sub_iam_members : - "${binding.sub}.${binding.role}" => binding - } - project = var.project_id - subscription = google_pubsub_subscription.default[each.value.sub].name - role = each.value.role - members = each.value.members -} diff --git a/modules/pubsub/outputs.tf b/modules/pubsub/outputs.tf index 0d149302..8218e2b3 100644 --- a/modules/pubsub/outputs.tf +++ b/modules/pubsub/outputs.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. @@ -19,7 +19,8 @@ output "id" { value = local.topic_id_static depends_on = [ google_pubsub_topic.default, - google_pubsub_topic_iam_binding.default + google_pubsub_topic_iam_binding.authoritative, + google_pubsub_topic_iam_binding.bindings ] } @@ -39,7 +40,8 @@ output "subscription_id" { for k, v in google_pubsub_subscription.default : k => v.id } depends_on = [ - google_pubsub_subscription_iam_binding.default + google_pubsub_subscription_iam_binding.authoritative, + google_pubsub_subscription_iam_binding.bindings ] } @@ -47,7 +49,8 @@ output "subscriptions" { description = "Subscription resources." value = google_pubsub_subscription.default depends_on = [ - google_pubsub_subscription_iam_binding.default + google_pubsub_subscription_iam_binding.authoritative, + google_pubsub_subscription_iam_binding.bindings ] } @@ -55,6 +58,7 @@ output "topic" { description = "Topic resource." value = google_pubsub_topic.default depends_on = [ - google_pubsub_topic_iam_binding.default + google_pubsub_topic_iam_binding.authoritative, + google_pubsub_topic_iam_binding.bindings ] } diff --git a/modules/pubsub/variables.tf b/modules/pubsub/variables.tf index afefb4a8..370c42fa 100644 --- a/modules/pubsub/variables.tf +++ b/modules/pubsub/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. @@ -14,48 +14,41 @@ * limitations under the License. */ -variable "bigquery_subscription_configs" { - description = "Configuration parameters for BigQuery subscriptions." - type = map(object({ - table = string - use_topic_schema = bool - write_metadata = bool - drop_unknown_fields = bool - })) - default = {} -} - -variable "dead_letter_configs" { - description = "Per-subscription dead letter policy configuration." - type = map(object({ - topic = string - max_delivery_attempts = number - })) - default = {} -} - -variable "defaults" { - description = "Subscription defaults for options." - type = object({ - ack_deadline_seconds = number - message_retention_duration = string - retain_acked_messages = bool - expiration_policy_ttl = string - filter = string - }) - default = { - ack_deadline_seconds = null - message_retention_duration = null - retain_acked_messages = null - expiration_policy_ttl = null - filter = null - } -} - variable "iam" { description = "IAM bindings for topic in {ROLE => [MEMBERS]} format." type = map(list(string)) default = {} + nullable = false +} + +variable "iam_bindings" { + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." + type = map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + nullable = false + default = {} +} + +variable "iam_bindings_additive" { + description = "Keyring individual additive IAM bindings. Keys are arbitrary." + type = map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + nullable = false + default = {} } variable "kms_key" { @@ -68,6 +61,7 @@ variable "labels" { description = "Labels." type = map(string) default = {} + nullable = false } variable "message_retention_duration" { @@ -86,23 +80,11 @@ variable "project_id" { type = string } -variable "push_configs" { - description = "Push subscription configurations." - type = map(object({ - attributes = map(string) - endpoint = string - oidc_token = object({ - audience = string - service_account_email = string - }) - })) - default = {} -} - variable "regions" { description = "List of regions used to set persistence policy." type = list(string) default = [] + nullable = false } variable "schema" { @@ -115,23 +97,72 @@ variable "schema" { default = null } -variable "subscription_iam" { - description = "IAM bindings for subscriptions in {SUBSCRIPTION => {ROLE => [MEMBERS]}} format." - type = map(map(list(string))) - default = {} -} - variable "subscriptions" { description = "Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null." type = map(object({ - labels = map(string) - options = object({ - ack_deadline_seconds = number - message_retention_duration = string - retain_acked_messages = bool - expiration_policy_ttl = string - filter = string - }) + labels = optional(map(string)) + ack_deadline_seconds = optional(number) + message_retention_duration = optional(string) + retain_acked_messages = optional(bool, false) + expiration_policy_ttl = optional(string) + filter = optional(string) + enable_message_ordering = optional(bool, false) + enable_exactly_once_delivery = optional(bool, false) + dead_letter_policy = optional(object({ + topic = string + max_delivery_attempts = optional(number) + })) + retry_policy = optional(object({ + minimum_backoff = optional(number) + maximum_backoff = optional(number) + })) + + bigquery = optional(object({ + table = string + use_topic_schema = optional(bool, false) + write_metadata = optional(bool, false) + drop_unknown_fields = optional(bool, false) + })) + cloud_storage = optional(object({ + bucket = string + filename_prefix = optional(string) + filename_suffix = optional(string) + max_duration = optional(string) + max_bytes = optional(number) + avro_config = optional(object({ + write_metadata = optional(bool, false) + })) + })) + push = optional(object({ + endpoint = string + attributes = optional(map(string)) + no_wrapper = optional(bool, false) + oidc_token = optional(object({ + audience = optional(string) + service_account_email = string + })) + })) + + iam = optional(map(list(string)), {}) + iam_bindings = optional(map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) + iam_bindings_additive = optional(map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })), {}) })) - default = {} + default = {} + nullable = false } diff --git a/modules/pubsub/versions.tf b/modules/pubsub/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/pubsub/versions.tf +++ b/modules/pubsub/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/secret-manager/versions.tf b/modules/secret-manager/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/secret-manager/versions.tf +++ b/modules/secret-manager/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/service-directory/versions.tf b/modules/service-directory/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/service-directory/versions.tf +++ b/modules/service-directory/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/source-repository/README.md b/modules/source-repository/README.md index a62013fa..c60ba7e4 100644 --- a/modules/source-repository/README.md +++ b/modules/source-repository/README.md @@ -75,13 +75,13 @@ module "repo" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L60) | Repository name. | string | ✓ | | -| [project_id](variables.tf#L65) | Project used for resources. | string | ✓ | | +| [name](variables.tf#L61) | Repository name. | string | ✓ | | +| [project_id](variables.tf#L66) | Project used for resources. | string | ✓ | | | [group_iam](variables.tf#L17) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string)) | | {} | | [iam](variables.tf#L24) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [iam_bindings](variables.tf#L31) | Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}. | map(object({…})) | | {} | -| [iam_bindings_additive](variables.tf#L45) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | -| [triggers](variables.tf#L70) | Cloud Build triggers. | map(object({…})) | | {} | +| [iam_bindings](variables.tf#L31) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables.tf#L46) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [triggers](variables.tf#L71) | Cloud Build triggers. | map(object({…})) | | {} | ## Outputs diff --git a/modules/source-repository/iam.tf b/modules/source-repository/iam.tf index be0cf688..1b225d1b 100644 --- a/modules/source-repository/iam.tf +++ b/modules/source-repository/iam.tf @@ -44,7 +44,7 @@ resource "google_sourcerepo_repository_iam_binding" "bindings" { for_each = var.iam_bindings project = var.project_id repository = google_sourcerepo_repository.default.name - role = each.key + role = each.value.role members = each.value.members dynamic "condition" { for_each = each.value.condition == null ? [] : [""] diff --git a/modules/source-repository/variables.tf b/modules/source-repository/variables.tf index ce1c34e7..23bfa789 100644 --- a/modules/source-repository/variables.tf +++ b/modules/source-repository/variables.tf @@ -29,9 +29,10 @@ variable "iam" { } variable "iam_bindings" { - description = "Authoritative IAM bindings in {ROLE => {members = [], condition = {}}}." + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." type = map(object({ members = list(string) + role = string condition = optional(object({ expression = string title = string diff --git a/modules/source-repository/versions.tf b/modules/source-repository/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/source-repository/versions.tf +++ b/modules/source-repository/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/modules/vpc-sc/versions.tf b/modules/vpc-sc/versions.tf index e4f7404f..91a91a31 100644 --- a/modules/vpc-sc/versions.tf +++ b/modules/vpc-sc/versions.tf @@ -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,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.80.0" # tftest + version = ">= 4.82.0" # tftest } } } diff --git a/tests/blueprints/factories/project_factory/examples/example.yaml b/tests/blueprints/factories/project_factory/examples/example.yaml index 5927caed..086fbd55 100644 --- a/tests/blueprints/factories/project_factory/examples/example.yaml +++ b/tests/blueprints/factories/project_factory/examples/example.yaml @@ -30,9 +30,10 @@ values: module.project-factory.module.projects["prj-app-1"].google_project.project[0]: auto_create_network: false billing_account: 012345-67890A-BCDEF0 - folder_id: null + folder_id: "12345678" labels: app: app-1 + environment: test team: foo name: test-pf-prj-app-1 org_id: null @@ -61,9 +62,10 @@ values: module.project-factory.module.projects["prj-app-2"].google_project.project[0]: auto_create_network: false billing_account: 012345-67890A-ABCDEF - folder_id: null + folder_id: "12345678" labels: app: app-1 + environment: test team: foo name: test-pf-prj-app-2 org_id: null diff --git a/tests/examples/variables.tf b/tests/examples/variables.tf index 3a5a3f75..9a65aa7a 100644 --- a/tests/examples/variables.tf +++ b/tests/examples/variables.tf @@ -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. @@ -69,6 +69,7 @@ variable "vpc" { default = { name = "vpc_name" self_link = "projects/xxx/global/networks/aaa" + id = "projects/xxx/global/networks/aaa" } } diff --git a/tests/fast/stages/s2_networking_a_peering/stage.yaml b/tests/fast/stages/s2_networking_a_peering/stage.yaml index 2c2ca3da..85b123af 100644 --- a/tests/fast/stages/s2_networking_a_peering/stage.yaml +++ b/tests/fast/stages/s2_networking_a_peering/stage.yaml @@ -14,4 +14,4 @@ counts: modules: 28 - resources: 151 + resources: 154 diff --git a/tests/fast/stages/s2_networking_b_vpn/stage.yaml b/tests/fast/stages/s2_networking_b_vpn/stage.yaml index 9cb8ee83..831bcd50 100644 --- a/tests/fast/stages/s2_networking_b_vpn/stage.yaml +++ b/tests/fast/stages/s2_networking_b_vpn/stage.yaml @@ -14,4 +14,4 @@ counts: modules: 30 - resources: 188 + resources: 191 diff --git a/tests/fast/stages/s2_networking_c_nva/stage.yaml b/tests/fast/stages/s2_networking_c_nva/stage.yaml index 3da9b352..e1ce4a05 100644 --- a/tests/fast/stages/s2_networking_c_nva/stage.yaml +++ b/tests/fast/stages/s2_networking_c_nva/stage.yaml @@ -14,4 +14,4 @@ counts: modules: 42 - resources: 197 + resources: 200 diff --git a/tests/fast/stages/s2_networking_d_separate_envs/stage.yaml b/tests/fast/stages/s2_networking_d_separate_envs/stage.yaml index f60257c4..e2b6fe64 100644 --- a/tests/fast/stages/s2_networking_d_separate_envs/stage.yaml +++ b/tests/fast/stages/s2_networking_d_separate_envs/stage.yaml @@ -14,4 +14,4 @@ counts: modules: 21 - resources: 168 + resources: 170 diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml b/tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml index 960ac523..bc557683 100644 --- a/tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml +++ b/tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml @@ -14,4 +14,4 @@ counts: modules: 36 - resources: 208 + resources: 211 diff --git a/tests/fast/stages/s3_project_factory/data/projects/project.yaml b/tests/fast/stages/s3_project_factory/data/projects/project.yaml index 18b5cdb4..922b4044 100644 --- a/tests/fast/stages/s3_project_factory/data/projects/project.yaml +++ b/tests/fast/stages/s3_project_factory/data/projects/project.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -parent_id: folders/012345678901 +parent: folders/012345678901 services: - storage.googleapis.com - stackdriver.googleapis.com diff --git a/tests/modules/apigee/all_psc_mode.tfvars b/tests/modules/apigee/all_psc_mode.tfvars new file mode 100644 index 00000000..41bafabb --- /dev/null +++ b/tests/modules/apigee/all_psc_mode.tfvars @@ -0,0 +1,47 @@ +project_id = "my-project" +organization = { + display_name = "My Organization" + description = "My Organization" + runtime_type = "CLOUD" + billing_type = "Pay-as-you-go" + database_encryption_key = "123456789" + analytics_region = "europe-west1" + disable_vpc_peering = true +} +envgroups = { + test = ["test.example.com"] + prod = ["prod.example.com"] +} +environments = { + apis-test = { + display_name = "APIs test" + description = "APIs Test" + envgroups = ["test"] + } + apis-prod = { + display_name = "APIs prod" + description = "APIs prod" + envgroups = ["prod"] + iam = { + "roles/viewer" = ["group:devops@myorg.com"] + } + } +} +instances = { + europe-west1 = { + environments = ["europe-west1"] + } + europe-west3 = { + environments = ["europe-west3"] + } +} +endpoint_attachments = { + endpoint-backend-1 = { + region = "europe-west1" + service_attachment = "projects/my-project-1/serviceAttachments/gkebackend1" + } + endpoint-backend-2 = { + region = "europe-west1" + service_attachment = "projects/my-project-2/serviceAttachments/gkebackend2" + } +} \ No newline at end of file diff --git a/tests/modules/apigee/all_psc_mode.yaml b/tests/modules/apigee/all_psc_mode.yaml new file mode 100644 index 00000000..c31c713a --- /dev/null +++ b/tests/modules/apigee/all_psc_mode.yaml @@ -0,0 +1,82 @@ +# 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_apigee_endpoint_attachment.endpoint_attachments["endpoint-backend-1"]: + endpoint_attachment_id: endpoint-backend-1 + location: europe-west1 + service_attachment: projects/my-project-1/serviceAttachments/gkebackend1 + google_apigee_endpoint_attachment.endpoint_attachments["endpoint-backend-2"]: + endpoint_attachment_id: endpoint-backend-2 + location: europe-west1 + service_attachment: projects/my-project-2/serviceAttachments/gkebackend2 + google_apigee_envgroup.envgroups["prod"]: + hostnames: + - prod.example.com + name: prod + google_apigee_envgroup.envgroups["test"]: + hostnames: + - test.example.com + name: test + google_apigee_envgroup_attachment.envgroup_attachments["apis-prod-prod"]: + environment: apis-prod + google_apigee_envgroup_attachment.envgroup_attachments["apis-test-test"]: + environment: apis-test + google_apigee_environment.environments["apis-prod"]: + description: APIs prod + display_name: APIs prod + name: apis-prod + google_apigee_environment.environments["apis-test"]: + description: APIs Test + display_name: APIs test + name: apis-test + google_apigee_environment_iam_binding.binding["apis-prod-roles/viewer"]: + condition: [] + env_id: apis-prod + members: + - group:devops@myorg.com + role: roles/viewer + google_apigee_instance.instances["europe-west3"]: + description: Terraform-managed + disk_encryption_key_name: null + display_name: null + location: europe-west3 + name: instance-europe-west3 + google_apigee_instance.instances["europe-west1"]: + description: Terraform-managed + disk_encryption_key_name: null + display_name: null + location: europe-west1 + name: instance-europe-west1 + google_apigee_organization.organization[0]: + analytics_region: europe-west1 + authorized_network: null + billing_type: Pay-as-you-go + description: null + display_name: null + project_id: my-project + retention: DELETION_RETENTION_UNSPECIFIED + runtime_database_encryption_key_name: '123456789' + runtime_type: CLOUD + disable_vpc_peering: true + +counts: + google_apigee_endpoint_attachment: 2 + google_apigee_envgroup: 2 + google_apigee_envgroup_attachment: 2 + google_apigee_environment: 2 + google_apigee_environment_iam_binding: 1 + google_apigee_instance: 2 + google_apigee_instance_attachment: 2 + google_apigee_organization: 1 \ No newline at end of file diff --git a/tests/modules/apigee/all.tfvars b/tests/modules/apigee/all_vpc_mode.tfvars similarity index 90% rename from tests/modules/apigee/all.tfvars rename to tests/modules/apigee/all_vpc_mode.tfvars index 69ffb084..03626f76 100644 --- a/tests/modules/apigee/all.tfvars +++ b/tests/modules/apigee/all_vpc_mode.tfvars @@ -7,6 +7,7 @@ organization = { billing_type = "Pay-as-you-go" database_encryption_key = "123456789" analytics_region = "europe-west1" + disable_vpc_peering = false } envgroups = { test = ["test.example.com"] @@ -17,13 +18,11 @@ environments = { display_name = "APIs test" description = "APIs Test" envgroups = ["test"] - regions = ["europe-west1"] } apis-prod = { display_name = "APIs prod" description = "APIs prod" envgroups = ["prod"] - regions = ["europe-west3"] iam = { "roles/viewer" = ["group:devops@myorg.com"] } @@ -33,10 +32,12 @@ instances = { europe-west1 = { runtime_ip_cidr_range = "10.0.4.0/22" troubleshooting_ip_cidr_range = "10.1.0.0/28" + environments = ["apis-test"] } europe-west3 = { runtime_ip_cidr_range = "10.0.6.0/22" troubleshooting_ip_cidr_range = "10.1.0.16/28" + environments = ["apis-prod"] } } endpoint_attachments = { @@ -48,4 +49,4 @@ endpoint_attachments = { region = "europe-west1" service_attachment = "projects/my-project-2/serviceAttachments/gkebackend2" } -} +} \ No newline at end of file diff --git a/tests/modules/apigee/all.yaml b/tests/modules/apigee/all_vpc_mode.yaml similarity index 97% rename from tests/modules/apigee/all.yaml rename to tests/modules/apigee/all_vpc_mode.yaml index c23eab27..2d39429c 100644 --- a/tests/modules/apigee/all.yaml +++ b/tests/modules/apigee/all_vpc_mode.yaml @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + values: google_apigee_endpoint_attachment.endpoint_attachments["endpoint-backend-1"]: endpoint_attachment_id: endpoint-backend-1 @@ -71,6 +72,7 @@ values: retention: DELETION_RETENTION_UNSPECIFIED runtime_database_encryption_key_name: '123456789' runtime_type: CLOUD + disable_vpc_peering: false counts: google_apigee_endpoint_attachment: 2 @@ -80,4 +82,4 @@ counts: google_apigee_environment_iam_binding: 1 google_apigee_instance: 2 google_apigee_instance_attachment: 2 - google_apigee_organization: 1 + google_apigee_organization: 1 \ No newline at end of file diff --git a/tests/modules/apigee/examples/minimal-cloud-no-org.yaml b/tests/modules/apigee/examples/minimal-cloud-no-org.yaml new file mode 100644 index 00000000..eee6638d --- /dev/null +++ b/tests/modules/apigee/examples/minimal-cloud-no-org.yaml @@ -0,0 +1,41 @@ +# 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.apigee.google_apigee_envgroup.envgroups["prod"]: + hostnames: + - prod.example.com + name: prod + org_id: organizations/project-id + module.apigee.google_apigee_envgroup_attachment.envgroup_attachments["apis-prod-prod"]: + environment: apis-prod + module.apigee.google_apigee_environment.environments["apis-prod"]: + description: Terraform-managed + display_name: APIs prod + name: apis-prod + org_id: organizations/project-id + module.apigee.google_apigee_instance.instances["europe-west1"]: + description: Terraform-managed + disk_encryption_key_name: null + display_name: null + ip_range: '' + location: europe-west1 + name: instance-europe-west1 + org_id: organizations/project-id + +counts: + google_apigee_envgroup: 1 + google_apigee_envgroup_attachment: 1 + google_apigee_environment: 1 + google_apigee_instance: 1 diff --git a/tests/modules/apigee/examples/minimal-cloud.yaml b/tests/modules/apigee/examples/minimal-cloud.yaml new file mode 100644 index 00000000..3a963de6 --- /dev/null +++ b/tests/modules/apigee/examples/minimal-cloud.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.apigee.google_apigee_envgroup.envgroups["prod"]: + hostnames: + - prod.example.com + name: prod + module.apigee.google_apigee_envgroup_attachment.envgroup_attachments["apis-prod-prod"]: + environment: apis-prod + module.apigee.google_apigee_environment.environments["apis-prod"]: + description: APIs Prod + display_name: APIs prod + name: apis-prod + module.apigee.google_apigee_instance.instances["europe-west1"]: + description: Terraform-managed + disk_encryption_key_name: null + display_name: null + ip_range: 10.32.0.0/22,10.64.0.0/28 + location: europe-west1 + name: instance-europe-west1 + module.apigee.google_apigee_organization.organization[0]: + analytics_region: europe-west1 + authorized_network: projects/xxx/global/networks/aaa + billing_type: PAYG + description: null + disable_vpc_peering: false + display_name: null + project_id: project-id + retention: DELETION_RETENTION_UNSPECIFIED + runtime_database_encryption_key_name: null + runtime_type: CLOUD + +counts: + google_apigee_envgroup: 1 + google_apigee_envgroup_attachment: 1 + google_apigee_environment: 1 + google_apigee_instance: 1 + google_apigee_organization: 1 diff --git a/tests/modules/apigee/examples/no-peering.yaml b/tests/modules/apigee/examples/no-peering.yaml new file mode 100644 index 00000000..02c6a5ec --- /dev/null +++ b/tests/modules/apigee/examples/no-peering.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.apigee.google_apigee_envgroup.envgroups["prod"]: + hostnames: + - prod.example.com + name: prod + module.apigee.google_apigee_envgroup_attachment.envgroup_attachments["apis-prod-prod"]: + environment: apis-prod + module.apigee.google_apigee_environment.environments["apis-prod"]: + description: Terraform-managed + display_name: APIs prod + name: apis-prod + module.apigee.google_apigee_instance.instances["europe-west1"]: + description: Terraform-managed + disk_encryption_key_name: null + display_name: null + ip_range: '' + location: europe-west1 + name: instance-europe-west1 + module.apigee.google_apigee_organization.organization[0]: + analytics_region: europe-west1 + authorized_network: null + billing_type: PAYG + description: null + disable_vpc_peering: true + display_name: null + project_id: project-id + retention: DELETION_RETENTION_UNSPECIFIED + runtime_database_encryption_key_name: null + runtime_type: CLOUD + +counts: + google_apigee_envgroup: 1 + google_apigee_envgroup_attachment: 1 + google_apigee_environment: 1 + google_apigee_instance: 1 + google_apigee_organization: 1 diff --git a/tests/modules/apigee/instance_only_psc_mode.tfvars b/tests/modules/apigee/instance_only_psc_mode.tfvars new file mode 100644 index 00000000..05fb2cd7 --- /dev/null +++ b/tests/modules/apigee/instance_only_psc_mode.tfvars @@ -0,0 +1,13 @@ +project_id = "my-project" +organization = { + display_name = "My Organization" + description = "My Organization" + runtime_type = "CLOUD" + billing_type = "Pay-as-you-go" + database_encryption_key = "123456789" + analytics_region = "europe-west1" + disable_vpc_peering = true +} +instances = { + europe-west1 = {} +} \ No newline at end of file diff --git a/tests/modules/apigee/instance_only_psc_mode.yaml b/tests/modules/apigee/instance_only_psc_mode.yaml new file mode 100644 index 00000000..4583d7b4 --- /dev/null +++ b/tests/modules/apigee/instance_only_psc_mode.yaml @@ -0,0 +1,35 @@ +# 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_apigee_instance.instances["europe-west1"]: + description: Terraform-managed + disk_encryption_key_name: null + display_name: null + location: europe-west1 + name: instance-europe-west1 + google_apigee_organization.organization[0]: + analytics_region: europe-west1 + billing_type: Pay-as-you-go + description: null + display_name: null + project_id: my-project + retention: DELETION_RETENTION_UNSPECIFIED + runtime_database_encryption_key_name: '123456789' + runtime_type: CLOUD + disable_vpc_peering: true + +counts: + google_apigee_instance: 1 + google_apigee_organization: 1 \ No newline at end of file diff --git a/tests/modules/apigee/instance_only.tfvars b/tests/modules/apigee/instance_only_vpc_mode.tfvars similarity index 67% rename from tests/modules/apigee/instance_only.tfvars rename to tests/modules/apigee/instance_only_vpc_mode.tfvars index 58074946..2367a884 100644 --- a/tests/modules/apigee/instance_only.tfvars +++ b/tests/modules/apigee/instance_only_vpc_mode.tfvars @@ -2,6 +2,6 @@ project_id = "my-project" instances = { europe-west1 = { runtime_ip_cidr_range = "10.0.4.0/22" - troubleshooting_ip_cidr_range = "10.1.1.0.0/28" + troubleshooting_ip_cidr_range = "10.1.1.0/28" } -} +} \ No newline at end of file diff --git a/tests/modules/apigee/instance_only.yaml b/tests/modules/apigee/instance_only_vpc_mode.yaml similarity index 82% rename from tests/modules/apigee/instance_only.yaml rename to tests/modules/apigee/instance_only_vpc_mode.yaml index bc42a370..cf5e7841 100644 --- a/tests/modules/apigee/instance_only.yaml +++ b/tests/modules/apigee/instance_only_vpc_mode.yaml @@ -14,7 +14,10 @@ values: google_apigee_instance.instances["europe-west1"]: - ip_range: 10.0.4.0/22,10.1.1.0.0/28 + ip_range: 10.0.4.0/22,10.1.1.0/28 + description: Terraform-managed + disk_encryption_key_name: null + display_name: null location: europe-west1 name: "instance-europe-west1" org_id: organizations/my-project diff --git a/tests/modules/apigee/organization_only_psc_mode.tfvars b/tests/modules/apigee/organization_only_psc_mode.tfvars new file mode 100644 index 00000000..f4808db5 --- /dev/null +++ b/tests/modules/apigee/organization_only_psc_mode.tfvars @@ -0,0 +1,10 @@ +project_id = "my-project" +organization = { + display_name = "My Organization" + description = "My Organization" + runtime_type = "CLOUD" + billing_type = "PAYG" + database_encryption_key = "123456789" + analytics_region = "europe-west1" + disable_vpc_peering = true +} \ No newline at end of file diff --git a/tests/modules/apigee/organization_only_psc_mode.yaml b/tests/modules/apigee/organization_only_psc_mode.yaml new file mode 100644 index 00000000..2bc93b4f --- /dev/null +++ b/tests/modules/apigee/organization_only_psc_mode.yaml @@ -0,0 +1,29 @@ +# 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_apigee_organization.organization[0]: + analytics_region: europe-west1 + authorized_network: null + billing_type: PAYG + description: null + display_name: null + project_id: my-project + retention: DELETION_RETENTION_UNSPECIFIED + runtime_database_encryption_key_name: '123456789' + runtime_type: CLOUD + disable_vpc_peering: true + +counts: + google_apigee_organization: 1 diff --git a/tests/modules/apigee/organization_only.tfvars b/tests/modules/apigee/organization_only_vpc_mode.tfvars similarity index 100% rename from tests/modules/apigee/organization_only.tfvars rename to tests/modules/apigee/organization_only_vpc_mode.tfvars diff --git a/tests/modules/apigee/organization_only.yaml b/tests/modules/apigee/organization_only_vpc_mode.yaml similarity index 100% rename from tests/modules/apigee/organization_only.yaml rename to tests/modules/apigee/organization_only_vpc_mode.yaml diff --git a/tests/modules/apigee/tftest.yaml b/tests/modules/apigee/tftest.yaml index f4a9944e..6449de75 100644 --- a/tests/modules/apigee/tftest.yaml +++ b/tests/modules/apigee/tftest.yaml @@ -15,13 +15,16 @@ module: modules/apigee tests: - all: + all_psc_mode: + all_vpc_mode: endpoint_attachment_only: env_only: env_only_with_api_proxy_type: env_only_with_deployment_type: envgroup_only: - instance_only: + instance_only_psc_mode: + instance_only_vpc_mode: no_instances: - organization_only: + organization_only_psc_mode: + organization_only_vpc_mode: organization_retention: diff --git a/tests/modules/compute_vm/examples/disk-options.yaml b/tests/modules/compute_vm/examples/disk-options.yaml index 1a4d58b2..f2f1a053 100644 --- a/tests/modules/compute_vm/examples/disk-options.yaml +++ b/tests/modules/compute_vm/examples/disk-options.yaml @@ -29,7 +29,6 @@ values: - device_name: data1 disk_encryption_key_raw: null mode: READ_WRITE - source: test-data1 boot_disk: - auto_delete: true disk_encryption_key_raw: null diff --git a/tests/modules/gke_cluster_autopilot/examples/monitoring-config-kube-state.yaml b/tests/modules/gke_cluster_autopilot/examples/monitoring-config-kube-state.yaml new file mode 100644 index 00000000..32e5bad5 --- /dev/null +++ b/tests/modules/gke_cluster_autopilot/examples/monitoring-config-kube-state.yaml @@ -0,0 +1,30 @@ +# 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: + monitoring_config: + - enable_components: + - DAEMONSET + - DEPLOYMENT + - HPA + - POD + - STATEFULSET + - STORAGE + - SYSTEM_COMPONENTS + managed_prometheus: + - enabled: true + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster_autopilot/network_tags.tfvars b/tests/modules/gke_cluster_autopilot/network_tags.tfvars new file mode 100644 index 00000000..4b188f31 --- /dev/null +++ b/tests/modules/gke_cluster_autopilot/network_tags.tfvars @@ -0,0 +1,14 @@ +project_id = "my-project" +location = "europe-west1" +name = "cluster-1" +vpc_config = { + network = "default" + subnetwork = "default" +} +tags = [ + "deep-dark-wood", + "hello-gruffalo", + "my--precious---nodes", + "cluster-1-nodes", + "nodes-cluster-1", +] diff --git a/tests/modules/gke_cluster_autopilot/network_tags.yaml b/tests/modules/gke_cluster_autopilot/network_tags.yaml new file mode 100644 index 00000000..5ca48260 --- /dev/null +++ b/tests/modules/gke_cluster_autopilot/network_tags.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: + google_container_cluster.cluster: + node_pool_auto_config: + - network_tags: + - tags: + - cluster-1-nodes + - deep-dark-wood + - hello-gruffalo + - my--precious---nodes + - nodes-cluster-1 + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster_autopilot/tftest.yaml b/tests/modules/gke_cluster_autopilot/tftest.yaml new file mode 100644 index 00000000..18fc6235 --- /dev/null +++ b/tests/modules/gke_cluster_autopilot/tftest.yaml @@ -0,0 +1,18 @@ +# 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/gke-cluster-autopilot + +tests: + network_tags: diff --git a/tests/modules/gke_cluster_standard/examples/monitoring-config-control-plane.yaml b/tests/modules/gke_cluster_standard/examples/monitoring-config-control-plane.yaml new file mode 100644 index 00000000..b3108770 --- /dev/null +++ b/tests/modules/gke_cluster_standard/examples/monitoring-config-control-plane.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.cluster-1.google_container_cluster.cluster: + monitoring_config: + - enable_components: + - APISERVER + - CONTROLLER_MANAGER + - SCHEDULER + - SYSTEM_COMPONENTS + managed_prometheus: + - enabled: true + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster_standard/examples/monitoring-config-disable-all.yaml b/tests/modules/gke_cluster_standard/examples/monitoring-config-disable-all.yaml new file mode 100644 index 00000000..1b5576a4 --- /dev/null +++ b/tests/modules/gke_cluster_standard/examples/monitoring-config-disable-all.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.google_container_cluster.cluster: + monitoring_config: + - enable_components: [] + managed_prometheus: + - enabled: false + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster_standard/examples/monitoring-config-kube-state.yaml b/tests/modules/gke_cluster_standard/examples/monitoring-config-kube-state.yaml new file mode 100644 index 00000000..32e5bad5 --- /dev/null +++ b/tests/modules/gke_cluster_standard/examples/monitoring-config-kube-state.yaml @@ -0,0 +1,30 @@ +# 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: + monitoring_config: + - enable_components: + - DAEMONSET + - DEPLOYMENT + - HPA + - POD + - STATEFULSET + - STORAGE + - SYSTEM_COMPONENTS + managed_prometheus: + - enabled: true + +counts: + google_container_cluster: 1 diff --git a/tests/modules/kms/examples/basic.yaml b/tests/modules/kms/examples/basic.yaml index e29297a1..30f40627 100644 --- a/tests/modules/kms/examples/basic.yaml +++ b/tests/modules/kms/examples/basic.yaml @@ -18,37 +18,26 @@ values: name: key-a purpose: ENCRYPT_DECRYPT rotation_period: null - skip_initial_version_creation: null - timeouts: null + skip_initial_version_creation: false module.kms.google_kms_crypto_key.default["key-b"]: labels: null name: key-b purpose: ENCRYPT_DECRYPT rotation_period: 604800s - skip_initial_version_creation: null - timeouts: null + skip_initial_version_creation: false module.kms.google_kms_crypto_key.default["key-c"]: labels: env: test name: key-c purpose: ENCRYPT_DECRYPT rotation_period: null - skip_initial_version_creation: null - timeouts: null - module.kms.google_kms_crypto_key_iam_binding.default["key-a.roles/cloudkms.admin"]: + skip_initial_version_creation: false + module.kms.google_kms_crypto_key_iam_binding.authoritative["key-a.roles/cloudkms.admin"]: condition: [] members: - user:user3@example.com role: roles/cloudkms.admin - ? module.kms.google_kms_crypto_key_iam_member.default["key-b.roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user4@example.com"] - : condition: [] - member: user:user4@example.com - role: roles/cloudkms.cryptoKeyEncrypterDecrypter - ? module.kms.google_kms_crypto_key_iam_member.default["key-b.roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user5@example.com"] - : condition: [] - member: user:user5@example.com - role: roles/cloudkms.cryptoKeyEncrypterDecrypter - module.kms.google_kms_crypto_key_iam_member.members["key-b-am1"]: + module.kms.google_kms_crypto_key_iam_member.members["key-b-iam1"]: condition: [] member: user:am1@example.com role: roles/cloudkms.cryptoKeyEncrypterDecrypter @@ -56,23 +45,9 @@ values: location: europe-west1 name: test project: my-project - timeouts: null - module.kms.google_kms_key_ring_iam_member.default["roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user1@example.com"]: - condition: [] - member: user:user1@example.com - role: roles/cloudkms.cryptoKeyEncrypterDecrypter - module.kms.google_kms_key_ring_iam_member.default["roles/cloudkms.cryptoKeyEncrypterDecrypteruser:user2@example.com"]: - condition: [] - member: user:user2@example.com - role: roles/cloudkms.cryptoKeyEncrypterDecrypter counts: google_kms_crypto_key: 3 google_kms_crypto_key_iam_binding: 1 - google_kms_crypto_key_iam_member: 3 + google_kms_crypto_key_iam_member: 1 google_kms_key_ring: 1 - google_kms_key_ring_iam_member: 2 - modules: 1 - resources: 10 - -outputs: {} diff --git a/tests/modules/kms/examples/purpose.yaml b/tests/modules/kms/examples/purpose.yaml index c08779b2..9f97ad52 100644 --- a/tests/modules/kms/examples/purpose.yaml +++ b/tests/modules/kms/examples/purpose.yaml @@ -15,25 +15,19 @@ values: module.kms.google_kms_crypto_key.default["key-a"]: name: key-a - purpose: ENCRYPT_DECRYPT - module.kms.google_kms_crypto_key.default["key-b"]: - name: key-b - purpose: ENCRYPT_DECRYPT - module.kms.google_kms_crypto_key.default["key-c"]: - name: key-c purpose: ASYMMETRIC_SIGN version_template: - algorithm: EC_SIGN_P384_SHA384 - protection_level: SOFTWARE + protection_level: HSM module.kms.google_kms_key_ring.default[0]: location: europe-west1 name: test project: my-project counts: - google_kms_crypto_key: 3 + google_kms_crypto_key: 1 google_kms_key_ring: 1 modules: 1 - resources: 4 + resources: 2 outputs: {} diff --git a/tests/modules/net_vpc/examples/factory.yaml b/tests/modules/net_vpc/examples/factory.yaml index fb348397..50aa01e1 100644 --- a/tests/modules/net_vpc/examples/factory.yaml +++ b/tests/modules/net_vpc/examples/factory.yaml @@ -48,8 +48,7 @@ values: tags: null timeouts: null module.vpc.google_compute_subnetwork.proxy_only["europe-west4/subnet-proxy"]: - description: Terraform-managed proxy-only subnet for Regional HTTPS or Internal - HTTPS LB. + description: Terraform-managed proxy-only subnet for Regional HTTPS, Internal HTTPS or Cross-Regional HTTPS Internal LB. ip_cidr_range: 10.1.0.0/24 ipv6_access_type: null log_config: [] @@ -59,6 +58,17 @@ values: region: europe-west4 role: ACTIVE timeouts: null + module.vpc.google_compute_subnetwork.proxy_only["australia-southeast2/subnet-proxy-global"]: + description: Terraform-managed proxy-only subnet for Regional HTTPS, Internal HTTPS or Cross-Regional HTTPS Internal LB. + ip_cidr_range: 10.4.0.0/24 + ipv6_access_type: null + log_config: [] + name: subnet-proxy-global + project: my-project + purpose: GLOBAL_MANAGED_PROXY + region: australia-southeast2 + role: ACTIVE + timeouts: null module.vpc.google_compute_subnetwork.psc["europe-west4/subnet-psc"]: description: Terraform-managed subnet for Private Service Connect (PSC NAT). ip_cidr_range: 10.2.0.0/24 @@ -127,9 +137,9 @@ values: counts: google_compute_network: 1 google_compute_route: 2 - google_compute_subnetwork: 5 + google_compute_subnetwork: 6 google_compute_subnetwork_iam_binding: 1 modules: 1 - resources: 9 + resources: 10 outputs: {} diff --git a/tests/modules/net_vpc/examples/proxy-only-subnets.yaml b/tests/modules/net_vpc/examples/proxy-only-subnets.yaml index 6e2069aa..cf32912d 100644 --- a/tests/modules/net_vpc/examples/proxy-only-subnets.yaml +++ b/tests/modules/net_vpc/examples/proxy-only-subnets.yaml @@ -17,7 +17,7 @@ values: 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. + description: Terraform-managed proxy-only subnet for Regional HTTPS, Internal HTTPS or Cross-Regional HTTPS Internal LB. ip_cidr_range: 10.0.1.0/24 log_config: [] name: regional-proxy @@ -25,6 +25,15 @@ values: purpose: REGIONAL_MANAGED_PROXY region: europe-west1 role: ACTIVE + module.vpc.google_compute_subnetwork.proxy_only["australia-southeast2/global-proxy"]: + description: Terraform-managed proxy-only subnet for Regional HTTPS, Internal HTTPS or Cross-Regional HTTPS Internal LB. + ip_cidr_range: 10.0.4.0/24 + log_config: [] + name: global-proxy + project: my-project + purpose: GLOBAL_MANAGED_PROXY + region: australia-southeast2 + 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 @@ -37,4 +46,4 @@ values: counts: google_compute_network: 1 - google_compute_subnetwork: 2 + google_compute_subnetwork: 3 diff --git a/tests/modules/net_vpc/examples/subnet-iam.yaml b/tests/modules/net_vpc/examples/subnet-iam.yaml index 68b03418..1b925f48 100644 --- a/tests/modules/net_vpc/examples/subnet-iam.yaml +++ b/tests/modules/net_vpc/examples/subnet-iam.yaml @@ -80,7 +80,7 @@ values: region: europe-west1 role: roles/compute.networkUser subnetwork: subnet-1 - module.vpc.google_compute_subnetwork_iam_binding.bindings["europe-west1/subnet-1.roles/compute.networkUser.test_condition"]: + module.vpc.google_compute_subnetwork_iam_binding.bindings["subnet-1-iam"]: condition: - description: null expression: resource.matchTag('123456789012/env', 'prod') @@ -91,7 +91,7 @@ values: region: europe-west1 role: roles/compute.networkUser subnetwork: subnet-1 - module.vpc.google_compute_subnetwork_iam_member.bindings["subnet-2-am1"]: + module.vpc.google_compute_subnetwork_iam_member.bindings["subnet-2-iam"]: condition: [] member: user:am1@example.com project: my-project diff --git a/tests/modules/project/examples/iam-bindings.yaml b/tests/modules/project/examples/iam-bindings.yaml index f1f09e36..c9fee925 100644 --- a/tests/modules/project/examples/iam-bindings.yaml +++ b/tests/modules/project/examples/iam-bindings.yaml @@ -23,7 +23,7 @@ values: project_id: foo-project-example skip_delete: false timeouts: null - module.project.google_project_iam_binding.bindings["roles/resourcemanager.projectIamAdmin"]: + module.project.google_project_iam_binding.bindings["iam_admin_conditional"]: condition: - description: null expression: "api.getAttribute(\n 'iam.googleapis.com/modifiedGrantsByRole',\ @@ -54,4 +54,3 @@ counts: resources: 4 outputs: {} - diff --git a/tests/modules/pubsub/examples/simple.yaml b/tests/modules/pubsub/examples/simple.yaml index 51094a51..6fe54ec6 100644 --- a/tests/modules/pubsub/examples/simple.yaml +++ b/tests/modules/pubsub/examples/simple.yaml @@ -16,14 +16,14 @@ values: module.pubsub.google_pubsub_topic.default: name: my-topic project: my-project - module.pubsub.google_pubsub_topic_iam_binding.default["roles/pubsub.subscriber"]: + module.pubsub.google_pubsub_topic_iam_binding.authoritative["roles/pubsub.subscriber"]: condition: [] members: - user:user1@example.com project: my-project role: roles/pubsub.subscriber topic: my-topic - module.pubsub.google_pubsub_topic_iam_binding.default["roles/pubsub.viewer"]: + module.pubsub.google_pubsub_topic_iam_binding.authoritative["roles/pubsub.viewer"]: condition: [] members: - group:foo@example.com diff --git a/tests/modules/pubsub/examples/subscription-iam.yaml b/tests/modules/pubsub/examples/subscription-iam.yaml index d0fa9fb6..42ed2565 100644 --- a/tests/modules/pubsub/examples/subscription-iam.yaml +++ b/tests/modules/pubsub/examples/subscription-iam.yaml @@ -13,10 +13,10 @@ # limitations under the License. values: - module.pubsub.google_pubsub_subscription_iam_binding.default["test-1.roles/pubsub.subscriber"]: + module.pubsub.google_pubsub_subscription_iam_binding.authoritative["test-1.roles/pubsub.subscriber"]: condition: [] members: - - user:user1@ludomagno.net + - user:user1@example.com project: my-project role: roles/pubsub.subscriber subscription: test-1 diff --git a/tests/modules/pubsub/examples/subscriptions.yaml b/tests/modules/pubsub/examples/subscriptions.yaml index a87a6d47..b1a94212 100644 --- a/tests/modules/pubsub/examples/subscriptions.yaml +++ b/tests/modules/pubsub/examples/subscriptions.yaml @@ -16,22 +16,22 @@ values: module.pubsub.google_pubsub_subscription.default["test-pull"]: bigquery_config: [] dead_letter_policy: [] - enable_exactly_once_delivery: null - enable_message_ordering: null + enable_exactly_once_delivery: False + enable_message_ordering: False filter: null labels: null message_retention_duration: 604800s name: test-pull project: my-project push_config: [] - retain_acked_messages: null + retain_acked_messages: False retry_policy: [] topic: my-topic module.pubsub.google_pubsub_subscription.default["test-pull-override"]: bigquery_config: [] dead_letter_policy: [] - enable_exactly_once_delivery: null - enable_message_ordering: null + enable_exactly_once_delivery: False + enable_message_ordering: False filter: null labels: test: override @@ -39,7 +39,7 @@ values: name: test-pull-override project: my-project push_config: [] - retain_acked_messages: true + retain_acked_messages: True retry_policy: [] topic: my-topic module.pubsub.google_pubsub_topic.default: